恼人的发电机bug

kjo*_*kjo 10 python generator

这个bug的原始上下文是一段太大而无法在这样的问题中发布的代码.我不得不将这段代码缩小到仍然显示错误的最小片段.这就是为什么下面显示的代码有些奇怪.

在下面的代码中,该类Foo可能被认为是一种令人费解的方式来获得类似的东西xrange.

class Foo(object):
    def __init__(self, n):
        self.generator = (x for x in range(n))

    def __iter__(self):
        for e in self.generator:
            yield e
Run Code Online (Sandbox Code Playgroud)

实际上,Foo似乎表现得非常像xrange:

for c in Foo(3):
    print c
# 0
# 1
# 2

print list(Foo(3))
# [0, 1, 2]
Run Code Online (Sandbox Code Playgroud)

现在,子类BarFoo仅增加了一个__len__方法:

class Bar(Foo):
    def __len__(self):
        return sum(1 for _ in self.generator)
Run Code Online (Sandbox Code Playgroud)

Bar行为就像Foofor-loop中使用时一样:

for c in Bar(3):
    print c
# 0
# 1
# 2
Run Code Online (Sandbox Code Playgroud)

但:

print list(Bar(3))
# []
Run Code Online (Sandbox Code Playgroud)

我的猜测是,在评估中list(Bar(3)),__len__方法Bar(3)被调用,从而耗尽了发生器.

(如果这个猜测是正确的,那么调用Bar(3).__len__是不必要的;毕竟,list(Foo(3))即使Foo没有__len__方法,也会产生正确的结果.)

这种情况是烦人:有没有很好的理由list(Foo(3)),并list(Bar(3))产生不同的结果.

是否有可能修复Bar(当然,没有摆脱它的__len__方法)list(Bar(3))返回[0, 1, 2]

Ser*_*sta 6

你的问题是Foo与xrange的行为不同:xrange在你每次询问它的iter方法时给你一个新的迭代器,而Foo总是给你一个相同的意思,这意味着一旦它耗尽了对象也是:

>>> a = Foo(3)
>>> list(a)
[0, 1, 2]
>>> list(a)
[]
>>> a = range(3)
>>> list(a)
[0, 1, 2]
>>> list(a)
[0, 1, 2]
Run Code Online (Sandbox Code Playgroud)

我可以通过向您的方法添加spys来轻松确认__len__调用该list方法:

class Bar(Foo):
    def __len__(self):
        print "LEN"
        return sum(1 for _ in self.generator)
Run Code Online (Sandbox Code Playgroud)

(我加了print "ITERATOR"Foo.__iter__).它产生:

>>> list(Bar(3))
LEN
ITERATOR
[]
Run Code Online (Sandbox Code Playgroud)

我只能想象两个解决方法:

  1. 我的首选之一:在每次调用返回一个新的迭代器__iter__Foo水平模仿xrange:

    class Foo(object):
        def __init__(self, n):
            self.n = n
    
        def __iter__(self):
            print "ITERATOR"
            return ( x for x in range(self.n))
    
    class Bar(Foo):
        def __len__(self):
            print "LEN"
            return sum(1 for _ in self.generator)
    
    Run Code Online (Sandbox Code Playgroud)

    我们得到正确的:

    >>> list(Bar(3))
    ITERATOR
    LEN
    ITERATOR
    [0, 1, 2]
    
    Run Code Online (Sandbox Code Playgroud)
  2. 替代方法:将len更改为不调用迭代器并Foo保持不变:

    class Bar(Foo):
        def __init__(self, n):
            self.len  = n
            super(Bar, self).__init__(n)
        def __len__(self):
            print "LEN"
            return self.len
    
    Run Code Online (Sandbox Code Playgroud)

    我们再来一次:

    >>> list(Bar(3))
    LEN
    ITERATOR
    [0, 1, 2]
    
    Run Code Online (Sandbox Code Playgroud)

    但是一旦第一个迭代器到达终点,Foo和Bar对象就会耗尽.

但我必须承认,我不知道你真正班级的背景......