小编典典

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

python

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

我测试了以下简单代码:

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中更新列表会更改另一个生成器变量的输出?我觉得这很奇怪。


阅读 208

收藏
2021-01-20

共1个答案

小编典典

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

早期绑定与后期绑定

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

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

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

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


由于实际上list不是a,因此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
[]
2021-01-20