如何编写一个生成器类?

81

我看到很多生成器函数的例子,但我想知道如何为类编写生成器。比方说,我想将斐波那契数列写成一个类。

class Fib:
    def __init__(self):
        self.a, self.b = 0, 1

    def __next__(self):
        yield self.a
        self.a, self.b = self.b, self.a+self.b

f = Fib()

for i in range(3):
    print(next(f))

输出:

<generator object __next__ at 0x000000000A3E4F68>
<generator object __next__ at 0x000000000A3E4F68>
<generator object __next__ at 0x000000000A3E4F68>
为什么值 self.a 没有被打印出来?同时,我如何为生成器编写 unittest

编写生成器类并不容易,特别是对于Python而言(如果你指的是生成器 -- https://docs.python.org/3/tutorial/classes.html#generators)。 - Konstantin Burlachenko
如果您的意思是实现可迭代协议,那么可以这样做(https://docs.python.org/3/tutorial/classes.html#iterators),这正是您代码片段所涉及的内容。 - Konstantin Burlachenko
5个回答

120

如何编写生成器类?

你已经接近成功了,正在编写一个迭代器类(我在答案末尾展示一个生成器),但是__next__每次使用next调用对象时都会被调用,返回一个生成器对象。相反,为了使你的代码能够以最少的更改和最少的代码行数工作,请使用__iter__,它使你的类实例化一个可迭代对象(这不是严格意义上的生成器):

class Fib:
    def __init__(self):
        self.a, self.b = 0, 1
    def __iter__(self):
        while True:
            yield self.a
            self.a, self.b = self.b, self.a+self.b

当我们将可迭代对象传递给 iter() 函数时,它会返回一个迭代器。
>>> f = iter(Fib())
>>> for i in range(3):
...     print(next(f))
...
0
1
1

为了使类本身成为一个迭代器,确实需要一个 __next__ 方法:
class Fib:
    def __init__(self):
        self.a, self.b = 0, 1        
    def __next__(self):
        return_value = self.a
        self.a, self.b = self.b, self.a+self.b
        return return_value
    def __iter__(self):
        return self

现在,由于iter仅返回实例本身,我们不需要调用它:

>>> f = Fib()
>>> for i in range(3):
...     print(next(f))
...
0
1
1

“为什么self.a的值没有被打印出来?”
以下是您的原始代码及我的注释:
class Fib:
    def __init__(self):
        self.a, self.b = 0, 1
        
    def __next__(self):
        yield self.a          # yield makes .__next__() return a generator!
        self.a, self.b = self.b, self.a+self.b

f = Fib()

for i in range(3):
    print(next(f))

每次调用 next(f),你都会得到生成器对象,它是由 __next__ 返回的:
<generator object __next__ at 0x000000000A3E4F68>
<generator object __next__ at 0x000000000A3E4F68>
<generator object __next__ at 0x000000000A3E4F68>

还有,我如何为生成器编写单元测试?

您仍需要为 Generator 实现 send 和 throw 方法。

from collections.abc import Iterator, Generator
import unittest

class Test(unittest.TestCase):
    def test_Fib(self):
        f = Fib()
        self.assertEqual(next(f), 0)
        self.assertEqual(next(f), 1)
        self.assertEqual(next(f), 1)
        self.assertEqual(next(f), 2) #etc...
    def test_Fib_is_iterator(self):
        f = Fib()
        self.assertIsInstance(f, Iterator)
    def test_Fib_is_generator(self):
        f = Fib()
        self.assertIsInstance(f, Generator)

现在:

>>> unittest.main(exit=False)
..F
======================================================================
FAIL: test_Fib_is_generator (__main__.Test)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<stdin>", line 7, in test_Fib_is_generator
AssertionError: <__main__.Fib object at 0x00000000031A6320> is not an instance of <class 'collections.abc.Generator'>

----------------------------------------------------------------------
Ran 3 tests in 0.001s

FAILED (failures=1)
<unittest.main.TestProgram object at 0x0000000002CAC780>

因此,让我们实现一个生成器对象,并利用集合模块中的Generator抽象基类(请参见其实现源代码),这意味着我们只需要实现sendthrow - 就可以免费获得close__iter__(返回self)和__next__(与.send(None)相同)(请参见Python协程数据模型):

class Fib(Generator):
    def __init__(self):
        self.a, self.b = 0, 1        
    def send(self, ignored_arg):
        return_value = self.a
        self.a, self.b = self.b, self.a+self.b
        return return_value
    def throw(self, type=None, value=None, traceback=None):
        raise StopIteration
    

并使用上述相同的测试:

>>> unittest.main(exit=False)
...
----------------------------------------------------------------------
Ran 3 tests in 0.002s

OK
<unittest.main.TestProgram object at 0x00000000031F7CC0>

Python 2

ABC Generator 只适用于 Python 3。如果没有 Generator,我们需要至少编写 close__iter____next__,以及上面定义的方法。

class Fib(object):
    def __init__(self):
        self.a, self.b = 0, 1        
    def send(self, ignored_arg):
        return_value = self.a
        self.a, self.b = self.b, self.a+self.b
        return return_value
    def throw(self, type=None, value=None, traceback=None):
        raise StopIteration
    def __iter__(self):
        return self
    def next(self):
        return self.send(None)
    def close(self):
        """Raise GeneratorExit inside generator.
        """
        try:
            self.throw(GeneratorExit)
        except (GeneratorExit, StopIteration):
            pass
        else:
            raise RuntimeError("generator ignored GeneratorExit")

请注意,我直接从Python 3 标准库复制了close,没有进行修改。

嗨,亚伦,非常感谢您的回复,这正是我在寻找的。了解更多关于迭代器和生成器的最佳方法是什么? - Pritam
@Pritam 我在这里的回答中详细阐述了这个主题:https://dev59.com/yXVC5IYBdhLWcg3woSpW#31042491 - Russia Must Remove Putin
@AaronHall 在第二个实例化中 f = iter(Fib())(在 "And now:" 之后),你可能想要实例化而不是将Fib类包装在 iter 函数中? - loxosceles
1
@loxosceles 的意图是演示迭代器协议的用法。迭代器具有一个 __iter__ 方法,当调用它时,返回自身。这似乎是多余的,但当迭代器对象被放置在迭代上下文中(如 for 循环或传递给可迭代构造函数)时,就会调用它。 - Russia Must Remove Putin
1
你对“如何编写生成器类?”的回答仅解释了如何实现迭代器接口。你的回答组织混乱。最后你展示了如何从cpython源代码中复制协程的实现...这与在类中实现生成器接口无关。从cpython源代码中复制未记录的实现代码是不好的做法,因为它可能会在小版本之间发生变化。不仅如此,由于它不是任何PEP规范的一部分,它可能只适用于cpython并在其他解释器上出现问题。 - Jared Deckard
显示剩余2条评论

10

__next__ 应该 返回 一个项目,而不是生成它。

您可以编写以下代码,其中Fib.__iter__ 返回适当的迭代器:

class Fib:
    def __init__(self, n):
        self.n = n
        self.a, self.b = 0, 1

    def __iter__(self):
        for i in range(self.n):
            yield self.a
            self.a, self.b = self.b, self.a+self.b

f = Fib(10)

for i in f:
    print i

或者通过定义__next__使每个实例本身成为迭代器。
class Fib:
    def __init__(self):
        self.a, self.b = 0, 1

    def __iter__(self):
        return self

    def __next__(self):
        x = self.a
        self.a, self.b = self.b, self.a + self.b
        return x

f = Fib()

for i in range(10):
    print next(f)

5
如果您给类一个__iter__()方法实现为生成器, 当调用时,“它将自动返回一个迭代器对象(技术上,一个生成器对象)”,因此使用的是该对象的__iter__()__next__()方法。
这就是我的意思:
class Fib:
    def __init__(self):
        self.a, self.b = 0, 1

    def __iter__(self):
        while True:
            value, self.a, self.b = self.a, self.b, self.a+self.b
            yield value

f = Fib()

for i, value in enumerate(f, 1):
    print(value)
    if i > 5:
        break

输出:

0
1
1
2
3
5

1
这使它成为一个可迭代对象,而不是生成器。 - Brian M. Sheldon
@Brian:更好了吗? - martineau
是的,这使它成为一个合适的生成器类。 - Brian M. Sheldon

3

在方法中使用yield会使该方法成为一个生成器,并调用该方法返回一个生成器迭代器next()期望一个实现了__next__()并返回一个项目的生成器迭代器。这就是为什么在__next__()中使用yield会导致在调用它时输出生成器迭代器的生成器类。

https://docs.python.org/3/glossary.html#term-generator

在实现接口时,您需要定义方法并将其映射到类实现上。在这种情况下,__next__()方法需要调用生成器迭代器。

class Fib:
    def __init__(self):
        self.a, self.b = 0, 1
        self.generator_iterator = self.generator()

    def __next__(self):
        return next(self.generator_iterator)

    def generator(self):
        while True:
            yield self.a
            self.a, self.b = self.b, self.a+self.b

f = Fib()

for i in range(3):
    print(next(f))
# 0
# 1
# 1

引用术语表通常是一件好事,但在这种情况下,我认为它不够精确,甚至是错误的。请参见我的答案以了解我的推理。如果术语表与实现相矛盾,则实现将是正确的来源。 - Russia Must Remove Putin
2
区分“生成器”和“生成器迭代器”是回答这个问题的重要部分。术语表是最精确的可用来源。术语表不会与实现相矛盾。你混淆了迭代器和协程与生成器,但它们并不相同。 - Jared Deckard

3

不要在__next__函数中使用yield,并且实现next以兼容Python2.7+。

代码

class Fib:
    def __init__(self):
        self.a, self.b = 0, 1
    def __next__(self):
        a = self.a
        self.a, self.b = self.b, self.a+self.b
        return a
    def next(self):
        return self.__next__()

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接