生成器的嵌套列表理解

时间:2019-05-26 18:20:51

标签: python generator list-comprehension

我在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

现在,即使生成器ilist包装,它仍然会产生相同的错误:

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还是预期的行为?如果这是预期的行为,您能否解释为什么这行不通?

3 个答案:

答案 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),RHaskell 更好地控制评估-从而控制懒惰的评估和承诺。但是在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 ifor 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]