Почему эксклюзивные эксклюзивные срезы и диапазоны?

Отказ от ответственности: я не спрашиваю, является ли аргумент stop верхней границы slice() и range() исключительным или как использовать эти функции.

Вызовы к функциям range и slice, а также нотации среза [start:stop] относятся к наборам целых чисел.

range([start], stop[, step])
slice([start], stop[, step])

Во всех этих, stop целое исключается.

Мне интересно, почему язык разработан таким образом.

Стоит ли stop равняться числу элементов в представленном целочисленном наборе, когда start равно 0 или опущено?

Это должно быть:

for i in range(start, stop):

выглядите следующим C-кодом?

for (i = start ; i < stop; i++) {

Ответ 1

Документация подразумевает, что это имеет несколько полезных свойств:

word[:2]    # The first two characters
word[2:]    # Everything except the first two characters

Это полезный инвариант операций среза: s[:i] + s[i:] равно s.

Для неотрицательных индексов длина среза - это разность индексов, если оба они находятся в пределах. Например, длина word[1:3] равна 2.

Я думаю, мы можем предположить, что функции диапазона действуют одинаково для последовательности.

Ответ 2

Вот мнение какого-то пользователя Google+:

[...] Меня поразила элегантность полуоткрытых промежутков. Особенно тот факт, что когда два слайса являются смежными, индекс конца первого слайса является индексом начала второго слайса, слишком красив, чтобы его игнорировать. Например, предположим, что вы разбили строку на три части с индексами я и j - части будут a [: i], a [i: j] и a [j:].

Google+ закрыт, поэтому ссылка больше не работает. Спойлер: это был Гвидо ван Россум.

Ответ 3

Немного поздно к этому вопросу, тем не менее, это пытается ответить на то, почему -part вашего вопроса:

Отчасти причина в том, что при обращении к памяти мы используем индексирование/смещение на основе нуля.

Самый простой пример - массив. Подумайте о "массиве из 6 элементов" в качестве места хранения 6 элементов данных. Если это начальное местоположение массива находится в адресе памяти 100, тогда данные, скажем, "яблоко\0" с 6 символами, сохраняются следующим образом:

memory/
array      contains
location   data
 100   ->   'a'
 101   ->   'p'
 102   ->   'p'
 103   ->   'l'
 104   ->   'e'
 105   ->   '\0'

Таким образом, для 6 элементов наш индекс идет от 100 до 105. Адреса генерируются с использованием base + offset, поэтому первый элемент находится в базовой ячейке памяти 100 + смещение 0 (т.е. 100 + 0), второй - 100 + 1, третий при 100 + 2,..., до 100 + 5 - это последнее место.

Это основная причина, по которой мы используем индексирование на основе нуля и приводим к языковым конструкциям, таким как for циклов в C:

for (int i = 0; i < LIMIT; i++)

или в Python:

for i in range(LIMIT):

Когда вы программируете на языке, таком как C, где вы имеете дело с указателями более прямо или даже более, тем не менее, эта схема основания + смещения становится намного более очевидной.

Из-за вышеизложенного многие языковые конструкции автоматически используют этот диапазон от начала до длины-1.

Вы можете найти эту статью о нулевой нумерации в Википедии интересной, а также этот вопрос от Software Engineering SE.

Пример:

В C, например, если у вас есть массив ar и вы индексируете его как ar[3] что действительно эквивалентно принятию (базового) адреса массива ar и добавлению 3 к нему => *(ar+3) что может привести к код, подобный этому, печатает содержимое массива, демонстрируя простой подход с базой + смещение:

for(i = 0; i < 5; i++)
   printf("%c\n", *(ar + i));

действительно эквивалентно

for(i = 0; i < 5; i++)
   printf("%c\n", ar[i]);

Ответ 4

Вот еще одна причина, по которой исключительная верхняя граница - более здравый подход:

Предположим, вы хотели написать функцию, которая применяет некоторое преобразование к подпоследовательности элементов в списке. Если бы интервалы должны были использовать инклюзивную верхнюю границу, как вы предлагаете, вы можете наивно попробовать написать ее как:

def apply_range_bad(lst, transform, start, end):
     """Applies a transform on the elements of a list in the range [start, end]"""
     left = lst[0 : start-1]
     middle = lst[start : end]
     right = lst[end+1 :]
     return left + [transform(i) for i in middle] + right

На первый взгляд это кажется простым и правильным, но, к сожалению, это неправильно.

Что произойдет, если:

  • start == 0
  • end == 0
  • end < 0

? В общем, могут быть еще более граничные случаи, которые вы должны учитывать. Кто хочет тратить время на размышления обо всем этом? (Эти проблемы возникают из-за того, что с использованием инклюзивных нижних и верхних границ нет встроенного способа выражения пустого интервала.)

Вместо этого, используя модель, где верхние границы являются исключительными, разделение списка на отдельные фрагменты проще, более элегантно и, следовательно, менее подвержено ошибкам:

def apply_range_good(lst, transform, start, end):
     """Applies a transform on the elements of a list in the range [start, end)"""
     left = lst[0:start]
     middle = lst[start:end]
     right = lst[end:]
     return left + [transform(i) for i in middle] + right

(Обратите внимание, что apply_range_good не преобразует lst[end], он также рассматривает end как эксклюзивную верхнюю границу. Пытаясь заставить его использовать инклюзивную верхнюю границу, все же будут иметь некоторые из проблем, о которых я упоминал ранее. -боты обычно хлопотны.)

(В основном адаптирован из старого сообщения о включении верхних границ на другом языке сценариев.)

Ответ 5

Элегантность VS Очевидность

Честно говоря, я думал, что способ нарезки на Python довольно противоречивый, он фактически торгует так называемой элегантностью с большей обработкой мозга, поэтому вы можете видеть, что fooobar.com/info/504/... имеет более 2Ks upvotes, Я думаю, это потому, что многие люди этого не понимают.

Например, следующий код уже вызвал головную боль для многих новичков Python.

x = [1,2,3,4]
print(x[0:1])
# Output is [1]

Мало того, что это сложно обрабатывать, его также трудно объяснить правильно, например, объяснение приведенного выше кода будет принимать нулевой элемент до элемента перед первым элементом.

Теперь посмотрим на Ruby, который использует верхнюю границу включительно.

x = [1,2,3,4]
puts x[0..1]
# Output is [1,2]

Чтобы быть откровенным, я действительно думал, что Ruby способ нарезки лучше для мозга.

Конечно, когда вы разбиваете список на 2 части на основе индекса, эксклюзивный подход с верхней границей приведет к созданию более красивого кода.

# Python
x = [1,2,3,4]
pivot = 2
print(x[:pivot]) # [1,2]
print(x[pivot:]) # [3,4]

Теперь давайте посмотрим на инклюзивный подход верхней границы

# Ruby
x = [1,2,3,4]
pivot = 2
puts x[0..(pivot-1)] # [1,2]
puts x[pivot..-1] # [3,4]

Очевидно, что код менее изящный, но здесь не так много обработки мозга.

Вывод

В конце концов, это действительно вопрос об Элегантности VS Очевидность, а дизайнеры Python предпочитают элегантность по очевидной. Зачем? Потому что Zen of Python утверждает, что Beautiful лучше, чем уродливый.