我正在运行一段代码,该代码在程序的某个部分意外出现逻辑错误。在调查本节时,我创建了一个测试文件来测试正在运行的语句集,并发现了一个看起来很奇怪的异常错误。
我测试了以下简单代码:
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中更新列表会更改另一个生成器变量的输出?对我来说这很奇怪。
答案 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]
但是,如果出于某些原因您不想创建列表,您也可以 在array
上async/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
,如果不需要在原始数组中保持值的顺序,则很有用。