使用条件生成器表达式的意外行为

时间:2019-01-17 23:11:04

标签: python generator variable-assignment generator-expression

我正在运行一段代码,该代码在程序的某个部分意外出现逻辑错误。在调查本节时,我创建了一个测试文件来测试正在运行的语句集,并发现了一个看起来很奇怪的异常错误。

我测试了以下简单代码:

array = [1, 2, 2, 4, 5] # Original array
f = (x for x in array if array.count(x) == 2) # Filters original
array = [5, 6, 1, 2, 9] # Updates original to something else

print(list(f)) # Outputs filtered

输出为:

>>> []

是的,没有。我期望过滤器理解能获得2中的项并输出,但是我没有得到:

# Expected output
>>> [2, 2]

当我注释掉第三行以再次对其进行测试时:

array = [1, 2, 2, 4, 5] # Original array
f = (x for x in array if array.count(x) == 2) # Filters original
### array = [5, 6, 1, 2, 9] # Ignore line

print(list(f)) # Outputs filtered

输出正确(您可以自己测试):

>>> [2, 2]

有一次我输出了变量f的类型:

array = [1, 2, 2, 4, 5] # Original array
f = (x for x in array if array.count(x) == 2) # Filters original
array = [5, 6, 1, 2, 9] # Updates original

print(type(f))
print(list(f)) # Outputs filtered

然后我得到了

>>> <class 'generator'>
>>> []

为什么在Python中更新列表会更改另一个生成器变量的输出?对我来说这很奇怪。

8 个答案:

答案 0 :(得分:59)

Python的生成器表达式后期绑定(请参阅PEP 289 -- Generator Expressions)(其他答案称为“惰性”):

  

早期绑定与后期绑定

     

经过大量讨论,决定应该立即评估[生成器表达式]的第一个(最外)表达式,并在执行生成器时评估其余表达式。

     

[...] Python对lambda表达式采用后期绑定方法,并且没有自动早期绑定的先例。人们认为引入新的范式会不必要地引入复杂性。

     

在探索了许多可能性之后,出现了一个共识,即绑定问题难以理解,应大力鼓励用户在立即使用其参数的函数中使用生成器表达式。对于更复杂的应用程序,完整的生成器定义在范围,生存期和绑定方面显而易见,因此始终是优越的。

这意味着仅在创建生成器表达式时评估最外面的for。因此,它实际上会在子表达式“ array”中将名称为in array的值绑定(实际上,此时它与iter(array)绑定了等效项)。但是,当您遍历生成器时,if array.count调用实际上是指当前名为array的对象。


由于实际上是list而不是array,因此我将答案的其余部分中的变量名称更改为更准确。

在第一种情况下,您要迭代的list和所计数的list会有所不同。就像您曾经使用过:

list1 = [1, 2, 2, 4, 5]
list2 = [5, 6, 1, 2, 9]
f = (x for x in list1 if list2.count(x) == 2)

因此,您检查list1中每个元素是否在list2中计数为两个。

您可以通过修改第二个列表轻松地验证这一点:

>>> lst = [1, 2, 2]
>>> f = (x for x in lst if lst.count(x) == 2)
>>> lst = [1, 1, 2]
>>> list(f)
[1]

如果遍历第一个列表并计入第一个列表中,它将返回[2, 2](因为第一个列表包含两个2)。如果迭代并计入第二个列表,则输出应为[1, 1]。但是,由于迭代了第一个列表(包含一个1),但是检查了第二个列表(包含两个1),因此输出只是一个1

使用生成器函数的解决方案

有几种可能的解决方案,如果不立即进行迭代,我通常不希望使用“生成器表达式”。一个简单的生成器函数足以使其正常工作:

def keep_only_duplicated_items(lst):
    for item in lst:
        if lst.count(item) == 2:
            yield item

然后像这样使用它:

lst = [1, 2, 2, 4, 5]
f = keep_only_duplicated_items(lst)
lst = [5, 6, 1, 2, 9]

>>> list(f)
[2, 2]

请注意,PEP(请参阅上面的链接)还指出,对于更复杂的事情,最好使用完整的生成器定义。

使用带有计数器的生成器功能的更好解决方案

一个更好的解决方案(避免二次运行时的行为,因为您遍历整个数组中的每个元素)将对元素计数一次(collections.Counter),然后在固定时间内进行查找(结果是线性时间):

from collections import Counter

def keep_only_duplicated_items(lst):
    cnts = Counter(lst)
    for item in lst:
        if cnts[item] == 2:
            yield item

附录:使用子类“可视化”发生的情况以及发生的时间

创建一个list子类很容易,当调用特定方法时,该子类将打印出来,因此可以验证它确实可以那样工作。

在这种情况下,我只是重写方法__iter__count,因为我对生成器表达式迭代哪个列表以及在哪个列表中计数感兴趣。方法主体实际上只是委托给超类并打印一些内容(因为它使用了super而不带参数和f字符串,因此它需要Python 3.6,但应该很容易适应其他Python版本):

class MyList(list):
    def __iter__(self):
        print(f'__iter__() called on {self!r}')
        return super().__iter__()

    def count(self, item):
        cnt = super().count(item)
        print(f'count({item!r}) called on {self!r}, result: {cnt}')
        return cnt

这是一个简单的子类,仅在调用__iter__count方法时进行打印:

>>> lst = MyList([1, 2, 2, 4, 5])

>>> f = (x for x in lst if lst.count(x) == 2)
__iter__() called on [1, 2, 2, 4, 5]

>>> lst = MyList([5, 6, 1, 2, 9])

>>> print(list(f))
count(1) called on [5, 6, 1, 2, 9], result: 1
count(2) called on [5, 6, 1, 2, 9], result: 1
count(2) called on [5, 6, 1, 2, 9], result: 1
count(4) called on [5, 6, 1, 2, 9], result: 0
count(5) called on [5, 6, 1, 2, 9], result: 1
[]

答案 1 :(得分:18)

正如其他人所提到的,Python generators很懒。运行此行时:

f = (x for x in array if array.count(x) == 2) # Filters original

实际上什么都没有发生。您已经声明了生成器函数f将如何工作。数组尚未查看。然后,您创建一个新数组,该数组将替换第一个数组,最后一个当您调用

print(list(f)) # Outputs filtered

生成器现在需要实际值,并开始从生成器f中提取它们。但是在这一点上,数组已经引用了第二个数组,因此您将获得一个空列表。

如果您需要重新分配列表,并且不能使用其他变量来保存列表,请考虑在第二行中创建列表而不是生成器:

f = [x for x in array if array.count(x) == 2] # Filters original
...
print(f)

答案 2 :(得分:9)

其他人已经解释了此问题的根本原因-生成器绑定到array局部变量的名称,而不是其值。

最pythonic的解决方案肯定是列表理解:

f = [x for x in array if array.count(x) == 2]

但是,如果出于某些原因您不想创建列表,您也可以 arrayasync/await RFC

f = (lambda array=array: (x for x in array if array.count(x) == 2))()

这里发生的是lambda在行运行时捕获了对array的引用,即使生成器后来被重新定义,也确保生成器可以看到您期望的变量。 / p>

请注意,它仍然绑定到变量(参考),而不是 value ,因此,例如,以下内容将打印[2, 2, 4, 4]

array = [1, 2, 2, 4, 5] # Original array

f = (lambda array=array: (x for x in array if array.count(x) == 2))() # Close over array
array.append(4)  # This *will* be captured

array = [5, 6, 1, 2, 9] # Updates original to something else

print(list(f)) # Outputs [2, 2, 4, 4]

在某些语言中这是一种常见的模式,但是它不是Python语言,所以只有在有很好的理由不使用列表推导(例如,array非常长或在嵌套的生成器理解中使用,而您担心内存)。

答案 3 :(得分:7)

如果这是此代码的主要用途,则说明您未正确使用生成器。使用列表理解而不是生成器理解。只需将括号替换为括号即可。如果您不知道,它将评估为列表。

array = [1, 2, 2, 4, 5]
f = [x for x in array if array.count(x) == 2]
array = [5, 6, 1, 2, 9]

print(f)
#[2, 2]

由于生成器的性质,您得到此响应。您正在调用生成器,但其内容将评估为[]

答案 4 :(得分:5)

生成器很懒,在您迭代它们之前,不会对它们进行评估。在这种情况下,您可以在window.addEventListener('resize', ()=>{ let height = window.innerHeight; let width = window.innerWidth; document.location = `http://myserver/axis-cgi/operator/param.cgi? action=update&Image.I0.Resolution=${width}x${height}` }); 处以生成器作为输入来创建list

答案 5 :(得分:4)

此问题的根本原因是生成器很懒。每次都对变量进行求值:

>>> l = [1, 2, 2, 4, 5, 5, 5]
>>> filtered = (x for x in l if l.count(x) == 2)
>>> l = [1, 2, 4, 4, 5, 6, 6]
>>> list(filtered)
[4]

遍历原始列表,并使用当前列表评估条件。在这种情况下,4在新列表中出现了两次,使其出现在结果中。它在结果中只出现一次,因为它在原始列表中只出现过一次。 6s在新列表中出现两次,但从未出现在旧列表中,因此也从不显示。

好奇的全功能自省(带注释的行是重要的行):

>>> l = [1, 2, 2, 4, 5]
>>> filtered = (x for x in l if l.count(x) == 2)
>>> l = [1, 2, 4, 4, 5, 6, 6]
>>> list(filtered)
[4]
>>> def f(original, new, count):
    current = original
    filtered = (x for x in current if current.count(x) == count)
    current = new
    return list(filtered)

>>> from dis import dis
>>> dis(f)
  2           0 LOAD_FAST                0 (original)
              3 STORE_DEREF              1 (current)

  3           6 LOAD_CLOSURE             0 (count)
              9 LOAD_CLOSURE             1 (current)
             12 BUILD_TUPLE              2
             15 LOAD_CONST               1 (<code object <genexpr> at 0x02DD36B0, file "<pyshell#17>", line 3>)
             18 LOAD_CONST               2 ('f.<locals>.<genexpr>')
             21 MAKE_CLOSURE             0
             24 LOAD_DEREF               1 (current)
             27 GET_ITER
             28 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             31 STORE_FAST               3 (filtered)

  4          34 LOAD_FAST                1 (new)
             37 STORE_DEREF              1 (current)

  5          40 LOAD_GLOBAL              0 (list)
             43 LOAD_FAST                3 (filtered)
             46 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             49 RETURN_VALUE
>>> f.__code__.co_varnames
('original', 'new', 'count', 'filtered')
>>> f.__code__.co_cellvars
('count', 'current')
>>> f.__code__.co_consts
(None, <code object <genexpr> at 0x02DD36B0, file "<pyshell#17>", line 3>, 'f.<locals>.<genexpr>')
>>> f.__code__.co_consts[1]
<code object <genexpr> at 0x02DD36B0, file "<pyshell#17>", line 3>
>>> dis(f.__code__.co_consts[1])
  3           0 LOAD_FAST                0 (.0)
        >>    3 FOR_ITER                32 (to 38)
              6 STORE_FAST               1 (x)
              9 LOAD_DEREF               1 (current)  # This loads the current list every time, as opposed to loading a constant.
             12 LOAD_ATTR                0 (count)
             15 LOAD_FAST                1 (x)
             18 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             21 LOAD_DEREF               0 (count)
             24 COMPARE_OP               2 (==)
             27 POP_JUMP_IF_FALSE        3
             30 LOAD_FAST                1 (x)
             33 YIELD_VALUE
             34 POP_TOP
             35 JUMP_ABSOLUTE            3
        >>   38 LOAD_CONST               0 (None)
             41 RETURN_VALUE
>>> f.__code__.co_consts[1].co_consts
(None,)

要重申:要迭代的列表仅加载一次。但是,条件或表达式中的所有闭包都是在每次迭代时从封闭范围中加载的。它们没有存储在常量中。

针对您的问题的最佳解决方案是创建一个引用原始列表的新变量,并将其用于生成器表达式中。

答案 6 :(得分:2)

生成器评估是“惰性”的-直到使用正确的引用将其实现后,该评估器才会执行。与您的一行:

再次以f类型查看输出:该对象是 generator ,而不是序列。它正在等待使用,它是各种迭代器。

您的生成器直到开始需要它的值时才进行评估。在那一点上,它使用该点上的可用值 ,而不是不是定义点。


“使其生效”的代码

这取决于您所说的“使其工作”。如果您希望f是过滤列表,请使用列表,而不是生成器:

f = [x for x in array if array.count(x) == 2] # Filters original

答案 7 :(得分:2)

生成器是懒惰的,并且在重新定义后耗尽生成器时将使用新定义的array。因此,输出正确。一种快速解决方案是通过用括号()代替括号[]来使用列表推导。

继续研究如何更好地编写逻辑,对循环中的值进行计数具有二次复杂度。对于可以在线性时间内工作的算法,您可以使用collections.Counter对值进行计数,并保留原始列表的副本

from collections import Counter

array = [1, 2, 2, 4, 5]   # original array
counts = Counter(array)   # count each value in array
old_array = array.copy()  # make copy
array = [5, 6, 1, 2, 9]   # updates array

# order relevant
res = [x for x in old_array if counts[x] >= 2]
print(res)
# [2, 2]

# order irrelevant
from itertools import chain
res = list(chain.from_iterable([x]*count for x, count in counts.items() if count >= 2))
print(res)
# [2, 2]

请注意,第二个版本甚至不需要old_array,如果不需要在原始数组中保持值的顺序,则很有用。