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

Sur*_*ari 57 python generator generator-expression variable-assignment

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

我测试了这个简单的代码:

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

输出是:

>>> []
Run Code Online (Sandbox Code Playgroud)

是的,没什么.我期待过滤器理解能够在数组中获取数量为2的项并输出它,但我没有得到:

# Expected output
>>> [2, 2]
Run Code Online (Sandbox Code Playgroud)

当我评论第三行再次测试时:

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

输出是正确的(你可以自己测试):

>>> [2, 2]
Run Code Online (Sandbox Code Playgroud)

有一次我输出了变量的类型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
Run Code Online (Sandbox Code Playgroud)

我得到了:

>>> <class 'generator'>
>>> []
Run Code Online (Sandbox Code Playgroud)

为什么用Python更新列表来改变另一个生成器变量的输出?这对我来说似乎很奇怪.

MSe*_*ert 59

Python的生成器表达式是后期绑定(参见PEP 289 - Generator Expressions)(其他答案称为"懒惰"):

早期结合与晚期结合

经过多次讨论后,决定应立即评估生成器表达式的第一个(最外层)for表达式,并在执行生成器时评估剩余的表达式.

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

因此,list1如果每个元素的计数list2为2 ,则检查每个元素.

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

>>> lst = [1, 2, 2]
>>> f = (x for x in lst if lst.count(x) == 2)
>>> lst = [1, 1, 2]
>>> list(f)
[1]
Run Code Online (Sandbox Code Playgroud)

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

解决方案使用生成器功能

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

def keep_only_duplicated_items(lst):
    for item in lst:
        if lst.count(item) == 2:
            yield item
Run Code Online (Sandbox Code Playgroud)

然后像这样使用它:

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

>>> list(f)
[2, 2]
Run Code Online (Sandbox Code Playgroud)

请注意,PEP(参见上面的链接)还指出,对于任何更复杂的事情,完整的生成器定义是可取的.

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

一个更好的解决方案(避免二次运行时行为,因为你遍历数组中每个元素的整个数组)将是count(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
Run Code Online (Sandbox Code Playgroud)

附录:使用子类"可视化"发生的事情以及发生的时间

创建一个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
Run Code Online (Sandbox Code Playgroud)

这是一个简单的子类,只是在调用__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
[]
Run Code Online (Sandbox Code Playgroud)

  • 这是解释质疑行为所涉及的所有微妙之处的唯一答案. (3认同)

Ste*_*ven 18

正如其他人所说,Python生成器是懒惰的.当这一行运行时:

f = (x for x in array if array.count(x) == 2) # Filters original
Run Code Online (Sandbox Code Playgroud)

实际上什么也没发生.您刚刚宣布了生成器函数f的工作方式.尚未查看数组.然后,您创建一个替换第一个的新数组,最后在您调用时

print(list(f)) # Outputs filtered
Run Code Online (Sandbox Code Playgroud)

发电机现在需要实际值并开始从发电机f中拉出它们.但此时,数组已经引用了第二个,所以你得到一个空列表.

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

f = [x for x in array if array.count(x) == 2] # Filters original
...
print(f)
Run Code Online (Sandbox Code Playgroud)

  • 这是**不正确**.正如/sf/answers/3797473011/解释``in array`中的`array`只是稍后绑定在`array.count`中的`array`.您也可以尝试解释https://tio.run/##RYxBCoAgAATvvmKPChKk1c2XRKdAEkRFDez1pgUFA3vYYcKVD@9krTZlKKwjR0M8SI6JY96Ibg8t0D6iwDh01@g@w@5Pl2lhUAqCkb/yJd7KspEQTVOtSZlqxmq9AQ (5认同)

sap*_*api 9

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

最pythonic的解决方案绝对是列表理解:

f = [x for x in array if array.count(x) == 2]
Run Code Online (Sandbox Code Playgroud)

但是,如果有某种原因,你不希望创建一个列表,你可以强制范围接近array:

f = (lambda array=array: (x for x in array if array.count(x) == 2))()
Run Code Online (Sandbox Code Playgroud)

这里发生的是lambda捕获array行运行时的引用,确保生成器看到您期望的变量,即使稍后重新定义变量.

请注意,这仍然绑定到变量(引用),而不是,因此,例如,将打印以下内容[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]
Run Code Online (Sandbox Code Playgroud)

这是某些语言中常见的模式,但它不是非常pythonic,所以只有在没有使用列表推导的一个非常好的理由时才会真正有意义(例如,如果array很长,或者在嵌套生成器理解中使用,而你关心的是记忆力.


Jab*_*Jab 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]
Run Code Online (Sandbox Code Playgroud)

由于生成器的性质,您得到此响应.当内容将评估时,你正在调用生成器[]


Mar*_*som 5

生成器是惰性的,在迭代它们之前不会对它们进行求值.在这种情况下,你创建了list与生成器作为输入,在print.

  • 这可能是我自己的答案,但它是不正确****(见MSeifert的答案),或试图解释https://tio.run/##RYxBCoAgAATvvmKPChKk1c2XRKdAEkRFDez1pgUFA3vYYcKVD@9krTZlKKwjR0M8SI6JY96Ibg8t0D6iwDh01@g@w@5Pl2lhUAqCkb/yJd7KspEQTVOtSZlqxmq9AQ (2认同)