Python 'list.extend(iterator)' гарантированно будет ленивым?
Резюме
Предположим, у меня есть iterator
который, поскольку элементы потребляются из него, выполняет некоторый побочный эффект, такой как изменение списка. Если я определю список l
и вызову l.extend(iterator)
, гарантируется ли, что extend
будет помещать элементы в l
один за другим, так как элементы из итератора потребляются, а не хранятся в буфере, а затем помещаются в все сразу?
Мои эксперименты
Я провел быстрый тест в Python 3.7 на своем компьютере, и list.extend
кажется ленивым, основываясь на этом тесте. (См. Код ниже.) Гарантируется ли это спецификацией, и если да, то где в спецификации упоминается?
(Кроме того, не стесняйтесь критиковать меня и говорить: "Это не Pythonic, вы дурак!" --though Я был бы признателен, если бы вы также ответили на вопрос, если хотите критиковать меня. собственное любопытство.)
Скажем, я определяю итератор, который добавляется в список во время его работы:
l = []
def iterator(k):
for i in range(5):
print([j in k for j in range(5)])
yield i
l.extend(iterator(l))
Вот примеры не ленивых (то есть буферизованных) и ленивых реализаций extend
:
def extend_nonlazy(l, iterator):
l += list(iterator)
def extend_lazy(l, iterator):
for i in iterator:
l.append(i)
Результаты
Вот что происходит, когда я запускаю обе известные реализации extend
.
Non-ленивый:
l = []
extend_nonlazy(l, iterator(l))
# output
[False, False, False, False, False]
[False, False, False, False, False]
[False, False, False, False, False]
[False, False, False, False, False]
[False, False, False, False, False]
# l = [0, 1, 2, 3, 4]
Ленивый:
l = []
extend_lazy(l, iterator(l))
[False, False, False, False, False]
[True, False, False, False, False]
[True, True, False, False, False]
[True, True, True, False, False]
[True, True, True, True, False]
Мои собственные эксперименты показывают, что нативный list.extend
похоже, работает как ленивая версия, но мой вопрос: гарантирует ли это спецификация Python?
Ответы
Ответ 1
Я не думаю, что проблема ленивая против не ленивых, потому что, либо в назначении слайса, либо в списке extend
, вам нужны все элементы итератора, и эти элементы используются сразу (в обычном случае). Вопрос, который вы подняли, более важен: эти операции атомарны или не атомарны? См. одно определение "атомарности" в Википедии:
Атомарность гарантирует, что каждая транзакция рассматривается как единая "единица", которая либо полностью завершается, либо полностью завершается неудачей.
Посмотрите на этот пример (CPython 3.6.8):
>>> def new_iterator(): return (1/(i-2) for i in range(5))
>>> L = []
>>> L[:] = new_iterator()
Traceback (most recent call last):
...
ZeroDivisionError: division by zero
>>> L
[]
Назначение среза не выполнено из-за исключения (i == 2
=> 1/(i - 2)
вызывает исключение), и список остался без изменений. Следовательно, операция назначения фрагмента является атомарной.
Теперь тот же пример с: extend
:
>>> L.extend(new_iterator())
Traceback (most recent call last):
...
ZeroDivisionError: division by zero
>>> L
[-0.5, -1.0]
Когда возникло исключение, два первых элемента уже были добавлены в список. Операция extend
не является атомарной, поскольку сбой не оставляет список без изменений.
Должна ли операция extend
быть атомарной или нет? Честно говоря, я понятия не имею об этом, но, как написано в ответе @wim, реальная проблема заключается в том, что в документации четко не указано (и, что еще хуже, документация утверждает, что extend
эквивалентен срезу присваивание, что не соответствует действительности в ссылочной реализации).
Ответ 2
Python list.extend(iterator)
гарантированно будет ленивым?
Наоборот, задокументировано, что
l.extend(iterable)
эквивалентно
l[len(l):] = iterable
В CPython такое присвоение среза сначала все равно преобразует генератор с правой стороны в список (см. здесь), т.е. он потребляет iterable
все сразу.
Показанный в вашем вопросе, строго говоря, противоречит документации. Ясообщил об ошибке в документации, но ее быстро закрыл Рэймонд Хеттингер.
Кроме того, есть менее запутанные способы продемонстрировать несоответствие. Просто определите неисправный генератор:
def gen():
yield 1
yield 2
yield 3
uh-oh
Теперь L.extend(gen())
изменит L
, а L[:] = gen()
- нет.