Почему Python допускает индексы срезов вне диапазона для последовательностей?

Так что я просто наткнулся на то, что мне кажется странной особенностью Python, и хотел кое-что прояснить.

Следующая манипуляция с массивом имеет некоторый смысл:

p = [1,2,3]
p[3:] = [4] 
p = [1,2,3,4]

Я полагаю, что это просто добавление этого значения к концу, верно?
Почему я могу это сделать, однако?

p[20:22] = [5,6]
p = [1,2,3,4,5,6]

И тем более это:

p[20:100] = [7,8]
p = [1,2,3,4,5,6,7,8]

Это просто кажется неправильной логикой. Похоже, это должно выдать ошибку!

Любое объяснение?
-Is это просто странная вещь, которую делает Python?
-Is есть ли цель?
-Or я думаю об этом неправильно?

Ответ 1

Часть вопроса о показателях вне диапазона

Логика среза автоматически обрезает индексы по длине последовательности.

Разрешение индексов срезов расширяться за конечные точки было сделано для удобства. Было бы больно иметь диапазон проверки каждого выражения, а затем корректировать пределы вручную, поэтому Python сделает это за вас.

Рассмотрим вариант использования желания отображать не более первых 50 символов текстового сообщения.

Самый простой способ (что сейчас делает Python):

preview = msg[:50]

Или трудный путь (проверяйте лимит самостоятельно):

n = len(msg)
preview = msg[:50] if n > 50 else msg

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

Часть вопроса относительно несоответствия длины назначений длине ввода

ОП также хотел знать обоснование для разрешения назначений, таких как p[20:100] = [7,8] где цель назначения имеет другую длину (80), чем длина замещающих данных (2).

Легче всего увидеть мотивацию по аналогии со строками. Рассмотрим "five little monkeys".replace("little", "humongous"). Обратите внимание, что у цели "маленький" есть только шесть букв, а у "огромных" - девять. Мы можем сделать то же самое со списками:

>>> s = list("five little monkeys")
>>> i = s.index('l')
>>> n = len('little')
>>> s[i : i+n ] = list("humongous")
>>> ''.join(s)
'five humongous monkeys'

Это все сводится к удобству.

До появления методов copy() и clear() это были популярные выражения:

s[:] = []           # clear a list
t = u[:]            # copy a list

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

s[:] = [x for x in s if not math.isnan(x)]   # filter-out NaN values

Надеюсь, что эти практические примеры дают хорошее представление о том, почему нарезка работает так, как она работает.

Ответ 2

В документации есть ваш ответ:

s[i:j]: часть s от i до j (примечание (4))

(4) Срез s от i до j определяется как последовательность элементов с индексом k такая, что i <= k < j. Если i или j больше, чем len(s), используйте len(s). Если i опущен или None, используйте 0. Если j опущено или None, используйте len(s). Если i больше или равен j, срез будет пустым.

Документация IndexError подтверждает это поведение:

исключение IndexError

Возникает, когда нижний индекс последовательности находится вне диапазона. (Индексы слайса молча усекаются, чтобы попасть в допустимый диапазон; если индекс не является целым числом, возбуждается TypeError.)

По сути, вещи типа p[20:100] сокращаются до p[len(p):len(p]. p[len(p):len(p] - это пустой фрагмент в конце списка, и назначение список к нему изменит конец списка, чтобы содержать указанный список, таким образом, он работает как добавление/расширение исходного списка.

Это поведение аналогично тому, что происходит, когда вы назначаете список пустому фрагменту в любом месте исходного списка. Например:

In [1]: p = [1, 2, 3, 4]

In [2]: p[2:2] = [42, 42, 42]

In [3]: p
Out[3]: [1, 2, 42, 42, 42, 3, 4]