我在python 3.7上有一些奇怪的行为,其中涉及生成器的嵌套列表理解。
这有效:
i = range(20)
n = [1, 2, 3]
result = [min(x + y for x in i) for y in n]
如果i
是生成器,则不起作用:
i = (p for p in range(20))
n = [1, 2, 3]
result = [min(x + y for x in i) for y in n]
这引起一个ValueError: min() arg is an empty sequence
现在,即使生成器i
用list
包装,它仍然会产生相同的错误:
i = (p for p in range(20))
n = [1, 2, 3]
result = [min(x + y for x in list(i)) for y in n]
这是python bug还是预期的行为?如果这是预期的行为,您能否解释为什么这行不通?
答案 0 :(得分:2)
在您的最后两个示例中,您都尝试在生成器用尽之后再次对其进行迭代。
在您的最后一个示例中,对list(i)
的每个值再次评估y
,因此i
将在第一次运行后耗尽。
您必须列出一次产生的值,如:
i = (p for p in range(20))
n = [1, 2, 3]
list_i = list(i)
result = [min(x + y for x in list_i) for y in n]
答案 1 :(得分:2)
在i = range(20)
中,range(20)
是生成发生器的保证。
i = (p for p in range(20))
已经是生成器了。
现在将您的列表表达式写为:
for y in [1, 2, 3]:
print(min(x + y for x in i))
## 1
## ...
## ValueError: min() arg is an empty sequence
您打印了1
,但是(在第一个调用中,生成器已耗尽)
然后进入下一轮
ValueError: min() arg is an empty sequence
因为生成器i
在y的第一个for循环调用中已经被消耗为1。
如果将i
定义为range(20)
,
每次调用for x in i
时,都会一次又一次地重新生成生成器。
您可以通过以下方式模仿range(20)
在做什么:
def gen():
return (p for p in range(20))
for y in [1, 2, 3]:
print(min(x + y for x in gen()))
# range() like gen() is a promise to generate the generator
## 1
## 2
## 3
现在,每次都重新创建生成器。
但是实际上,range
的确更酷:
i = range(20)
for y in [1, 2, 3]:
print(min(x + y for x in i))
## 1
## 2
## 3
内部生成器中的i
不是函数调用。
但是,尽管如此,它仍会在评估时产生一个新的生成器-
至少当在for循环中用作迭代器时。
这实际上是在Python中使用一个类并通过定义__iter__()
方法来实现的。定义了插入者中的行为-在这里特别是一种懒惰的行为。
要模仿这种行为,我们可以生成一个惰性生成器(lazy_gen
)。
class lazy_gen:
def __init__(self):
pass
def __iter__(self): # everytime when used as an iterator
return self.gen() # recreate the generator # real lazy behavior
def gen(self):
return (p for p in range(20))
我们可以这样使用:
i = lazy_gen()
for y in [1, 2, 3]:
print(min(x + y for x in i))
## 1
## 2
## 3
因此,这更好地反映了range()
的行为。
其他语言(功能语言),例如Lisp
家庭语言(常见的lisp,Racket,Scheme,Clojure),R
或Haskell
更好地控制评估-从而控制懒惰的评估和承诺。但是在Python中,对于此类实现和精细控制,必须在OOP中采用。
我的范围函数和类
最后,我弄清楚了范围函数必须如何大致实现。 (为了好玩,尽管我可以在我知道的Python源代码中查找它-但有时推理很有趣。)
class Myrange:
def __init__(self, start, end, step):
self.start = start
self.end = end
self.step = step
def __iter__(self):
return self.generate_range()
def generate_range(self):
x = self.start - self.step
while x + self.step < self.end:
x = x + self.step
yield x
def __repr__(self):
return "myrange({}, {})".format(self.start, self.end)
def myrange(start=None, end=None, step=1):
if start is None and end is None:
raise "Please provide at least one number for the range-limits."
elif start is not None and end is None:
_start = 0
_end = start
elif start is not None and end is not None:
_start = start
_end = end
else:
_start = 0
_end = end
_step = step
return Myrange(_start, _end, _step)
一个人可以像range函数一样使用它。
i = myrange(20)
n = [1, 2, 3]
result = [min(x + y for x in i) for y in n]
result
## [1, 2, 3]
i
## myrange(0, 20) # representation of a Myrange object.
myrange(20)
## myrange(0, 20)
list(myrange(3, 10))
## [3, 4, 5, 6, 7, 8, 9]
list(myrange(0, 10))
## [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
list(myrange(10))
## [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
list(myrange(0, 10, 2))
## [0, 2, 4, 6, 8]
list(myrange(3, 10, 2))
## [3, 5, 7, 9]
答案 2 :(得分:1)
for x in i
或for x in list(i)
的生成器在第一个for循环之后清空,相反,您需要事先将生成器转换为列表(本质上遍历生成器并清空生成器),然后使用该列表
请注意,这实际上违反了生成器的目的,因为现在这与第一种方法相同
In [14]: list(range(20)) == list(p for p in range(20))
Out[14]: True
因此更新后的代码将是
#Create generator and convert to list
i = list(p for p in range(20))
n = [1, 2, 3]
#Use that list in the list comprehension
result = [min(x + y for x in i) for y in n]
print(result)
输出将为
[1, 2, 3]
因此,因此,更好的方法是坚持第一种方法本身,或者可以将生成器内联,这再次与具有范围的第一种方法相同
n = [1, 2, 3]
result = [min(x + y for x in (p for p in range(20))) for y in n]
print(result)
#[1, 2, 3]