Почему присвоение конца списка списка через срез не вызывает индексацию?
Я работаю над разреженной реализацией списка и недавно реализованным назначением через срез. Это заставило меня обнаружить какое-то поведение в реализации Python list
, которое Я нахожу удивительный.
Учитывая пустой list
и назначение через срез:
>>> l = []
>>> l[100:] = ['foo']
Я бы ожидал IndexError
от list
здесь, потому что способ, которым это реализовано, означает, что элемент не может быть извлечен из указанного индекса::
>>> l[100]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: list index out of range
'foo'
не может быть даже извлечен из указанного фрагмента:
>>> l = []
>>> l[100:] = ['foo']
>>> l[100:]
[]
l[100:] = ['foo']
присоединяется к list
(т.е. l == ['foo']
после этого назначения) и, похоже, ведет себя таким образом, поскольку исходный BDFL версия. Я не могу найти эту функциональность в любом месте (*), но и CPython и PyPy ведут себя таким образом.
Присвоение по индексу вызывает ошибку:
>>> l[100] = 'bar'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: list assignment index out of range
Итак, почему назначение после конца list
через срез не поднимает IndexError
(или какая-то другая ошибка, я думаю)?
Чтобы прояснить следующие первые два комментария, этот вопрос относится именно к присваиванию, а не к поиску (cf. Почему индекс подрезки за пределами диапазона работает в Python?).
Вдаваясь в соблазн угадать и присваивать 'foo'
до l
при индексе 0, когда я явно указал индекс 100, не следует обычным Zen Python.
Рассмотрим случай, когда присвоение происходит далеко от инициализации, а индекс - переменная. Вызывающий абонент больше не может извлекать свои данные из указанного места.
Назначение среза до конца list
ведет себя несколько иначе, чем в примере выше:
>>> l = [None, None, None, None]
>>> l[3:] = ['bar']
>>> l[3:]
['bar']
(*) Это поведение определено в Примечание 4 5.6. Типы последовательности в официальной документации (спасибо elethan), но это не объясняет, почему это было бы желательно при назначении.
Примечание.. Я понимаю, как работает поиск, и вы можете видеть, как желательно быть совместимым с этим при назначении, но я искал процитированную причину того, почему приписывание срезу будет вести себя в этом путь. l[100:]
возвращает []
сразу после l[100:] = ['foo']
, но l[3:]
возвращает ['bar']
после l[3:] = ['bar']
поражает, если вы не знаете len(l)
, особенно если вы следуете за Python EAFP idiom.
Ответы
Ответ 1
Посмотрим, что на самом деле происходит:
>>> l = []
>>> l[100:] = ['foo']
>>> l[100:]
[]
>>> l
['foo']
Таким образом, назначение было действительно успешным, и элемент был помещен в список в качестве первого элемента.
Почему это происходит, потому что 100:
в позиции индексации преобразуется в объект slice
: slice(100, None, None)
:
>>> class Foo:
... def __getitem__(self, i):
... return i
...
>>> Foo()[100:]
slice(100, None, None)
Теперь класс slice
имеет метод indices
(я не могу найти его документацию на Python в Интернете, хотя), который при заданной длине последовательности даст (start, stop, stride)
, который будет скорректирован для длина этой последовательности.
>>> slice(100, None, None).indices(0)
(0, 0, 1)
Таким образом, когда этот срез применяется к последовательности длины 0, он ведет себя точно так же, как срез slice(0, 0, 1)
для фрагментов фрагментов, например. вместо foo[100:]
, вызывая ошибку, когда foo
является пустой последовательностью, она ведет себя так, как если бы была запрошена foo[0:0:1]
- это приведет к появлению пустого фрагмента при поиске.
Теперь код установщика должен работать правильно, когда l[100:]
использовался, когда l - последовательность, содержащая более 100 элементов. Чтобы заставить его работать, проще всего не изобретать колесо и просто использовать механизм indices
выше. Как недостаток, теперь он будет выглядеть немного странно в случаях кросс, но назначения срезов на срезы, которые являются "вне границ", будут помещены в конце текущей последовательности. (Однако выясняется, что в коде CPython мало повторного использования кода; list_ass_slice
по существу дублирует всю эту обработку индекса, хотя он также будет доступен через объект C-объекта slice).
Таким образом: , если начальный индекс среза больше или равен длине последовательности, результирующий срез ведет себя так, как если бы он был срезом нулевой ширины, начиная с конца последовательности. I.e.: if a >= len(l)
, l[a:]
ведет себя как l[len(l):len(l)]
для встроенных типов. Это верно для каждого из присваивания, поиска и удаления.
Желательность этого заключается в том, что он не нуждается в каких-либо исключениях. В методе slice.indices
не нужно обрабатывать какие-либо исключения - для последовательности длины l
, slice.indices(l)
всегда будет отображаться (start, end, stride)
индексов, которые могут использоваться для любого из присваивания, поиска и удаления, и это что обе start
и end
равны 0 <= v <= len(l)
.
Ответ 2
Для индексирования возникает ошибка должна, если данный индекс является вне границ, потому что нет допустимого значения по умолчанию, которое может быть возвращено. (Нельзя возвращать None
, потому что None
может быть допустимым элементом последовательности).
В отличие от этого, для нарезки, повышение ошибки не требуется, если какой-либо из индексов выходит за пределы, потому что допустимо возвращать пустую последовательность в качестве значения по умолчанию. И это также желательно сделать, потому что он обеспечивает последовательный путь для подпоследовательностей как между элементами, так и за пределами концов последовательности (таким образом, для вставки).
Как указано в "Примечания к последовательности последовательностей" , если начальное или конечное значение среза больше, чем len(seq)
, тогда len(seq)
используется вместо.
Итак, для a = [4, 5, 6]
выражения a[3:]
и a[100:]
указывают на пустую подпоследовательность, следующую за последним элементом в списке. Однако после присвоения среза с использованием этих выражений они могут больше не ссылаться на одно и то же, поскольку длина списка может быть изменена.
Таким образом, после присваивания a[3:] = [7]
срез a[3:]
вернет [7]
. Но после присвоения a[100:] = [8]
срез a[100:]
все равно вернет []
, потому что len(a)
все еще меньше 100
. И учитывая все сказанное выше, это именно то, что следует ожидать, если поддерживать согласованность между назначением среза и извлечением фрагментов.