Python`list.extend(iterator)`是否一定是懒惰的?

Kye*_*Shi 13 python iterator list lazy-evaluation data-structures

摘要

假设我有一个iterator当从中消耗元素时执行一些副作用的操作,例如修改列表。如果我定义一个list l和call l.extend(iterator),是否保证在迭代器中的元素被消耗时extend将元素l逐个推入一个元素,而不是保留在缓冲区中然后一次推入所有元素

我的实验

我在计算机上使用Python 3.7进行了快速测试,list.extend基于该测试似乎很懒。(请参见下面的代码。)这是否由规范保证,如果可以,则在规范中提到了什么?

(此外,请随时批评我,并说“这不是Python风格的,您这个傻瓜!”-尽管如果您也想回答我这个问题也回答了我,我将不胜感激。自己的好奇心。)

假设我定义了一个迭代器,该迭代器会在运行时推送到列表中:

l = []

def iterator(k):
  for i in range(5):
    print([j in k for j in range(5)])
    yield i

l.extend(iterator(l))
Run Code Online (Sandbox Code Playgroud)

以下是非延迟(即缓冲)与延迟可能extend实现的示例:

def extend_nonlazy(l, iterator):
  l += list(iterator)

def extend_lazy(l, iterator):
  for i in iterator:
    l.append(i)
Run Code Online (Sandbox Code Playgroud)

结果

这是当我运行的两个已知实现时发生的情况extend


非懒惰:

l = []
extend_nonlazy(l, iterator(l))
Run Code Online (Sandbox Code Playgroud)
# 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]
Run Code Online (Sandbox Code Playgroud)

懒:

# 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]
Run Code Online (Sandbox Code Playgroud)
[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]
Run Code Online (Sandbox Code Playgroud)

My own experimentation shows that native list.extend seems to work like the lazy version, but my question is: does the Python spec guarantee that?

jfe*_*ard 6

我不认为这个问题是懒惰 VS 非偷懒,因为,无论是在片分配或列表extend,你需要迭代的所有元素,但这些元素消耗一次(正常情况下)。您提出的问题更为重要:这些操作是原子操作还是非原子操作?请参阅Wikipedia中“原子性”的一种定义:

原子性保证将每笔交易视为一个单独的“单位”,该单位要么完全成功,要么完全失败。

看一下这个例子(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
[]
Run Code Online (Sandbox Code Playgroud)

切片分配由于异常而失败(i == 2=> 1/(i - 2)引发异常),并且列表保持不变。因此,切片分配操作是atomic

现在,使用以下示例extend

>>> L.extend(new_iterator())
Traceback (most recent call last):
...
ZeroDivisionError: division by zero
>>> L
[-0.5, -1.0]
Run Code Online (Sandbox Code Playgroud)

引发异常时,前两个元素已添加到列表中。该extend操作不是原子操作,因为失败不会使列表保持不变。

extend操作是否应为原子操作?坦白地说,我对此一无所知,但正如@wim的答案所写,真正的问题是文档中并未明确说明(更糟糕的是,文档断言extend等同于切片分配,在参考文献中是不正确的)实施)。


wim*_*wim 1

Pythonlist.extend(iterator)一定是懒惰的吗?

不。相反,有记录表明

l.extend(iterable)
Run Code Online (Sandbox Code Playgroud)

相当于

l[len(l):] = iterable
Run Code Online (Sandbox Code Playgroud)

在 CPython 中,这样的切片分配无论如何都会首先将右侧的生成器转换为列表(请参阅此处),即它iterable一次消耗所有内容。

严格来说,您问题中显示的示例与文档相矛盾。 我已经提交了一个文档错误,但很快就被 Raymond Hettinger 关闭了。

顺便说一句,有更简单的方法来证明这种差异。只需定义一个失败的生成器:

def gen():
    yield 1
    yield 2
    yield 3
    uh-oh
Run Code Online (Sandbox Code Playgroud)

现在L.extend(gen())会修改L,但L[:] = gen()不会。