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() - нет.