【问题标题】:How to write a generator class?如何编写生成器类?
【发布时间】:2017-08-16 10:50:10
【问题描述】:

我看到很多生成器函数的示例,但我想知道如何为类编写生成器。可以说,我想把斐波那契数列写成一门课。

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 generator fibonacci


【解决方案1】:

__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)

【讨论】:

    【解决方案2】:

    如何编写生成器类?

    你快到了,正在编写一个 Iterator 类(我在答案的末尾显示了一个生成器),但是每次使用 next 调用对象时都会调用 __next__,返回一个生成器对象。相反,为了让您的代码以最少的更改和最少的代码行运行,请使用 __iter__,这会使您的类实例化一个 iterable(从技术上讲,它不是一个 generator):

    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 的值没有被打印出来?

    这是您使用我的 cmets 的原始代码:

    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 实现一个发送和抛出方法

    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 抽象基类(参见其implementation 的源代码),这意味着我们只需要实现sendthrow - 给我们close__iter__(返回自我)和__next__(与.send(None)相同)免费(参见Python data model on coroutines):

    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 standard library 复制了close,没有进行任何修改。

    【讨论】:

    • 嗨 Aaron,非常感谢您的回复,这正是我想要的。了解更多关于迭代器和生成器的最佳方式是什么?
    • @Pritam 我在这个答案中对这个主题做了很多扩展:stackoverflow.com/a/31042491/541136
    • @AaronHall 在第二个实例化f = iter(Fib()) 中(在“And now:”之后),您可能打算在不将Fib 类包装在iter 函数中的情况下进行实例化?
    • @loxosceles 目的是演示迭代器协议的用法。迭代器有一个__iter__ 方法,当被调用时,它会返回自己。这似乎是多余的,但当迭代器对象被放置在迭代上下文中时(如 for 循环或传递给可迭代的构造函数),就会调用它。
    • 您对“如何编写生成器类?”的回复只解释如何实现迭代器接口。你的反应组织不连贯。最后,您将展示如何从 cpython 源代码复制协程的实现......这与在类中实现生成器接口无关。从 cpython 的源代码复制未记录的实现代码是不好的做法,因为它可能会在次要版本之间中断。不仅如此,而且由于它不是任何 PEP 规范的一部分,它可能只适用于 cpython 并在其他解释器上中断。
    【解决方案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__()
    

    【讨论】:

      【解决方案4】:

      如果你给类一个__iter__()方法implemented as a generator,“它会自动返回一个迭代器对象(技术上,一个生成器对象)”当调用时,所以那个对象的__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
      

      【讨论】:

      • 这使它成为可迭代的,而不是生成器
      • @Brian:更好?
      • 是的,这使它成为一个合适的生成器类
      【解决方案5】:

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

      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
      

      【讨论】:

      • 引用词汇表通常是一件好事,但在这种情况下,我认为它不精确到不正确的地步。请参阅我的答案以了解我的推理。如果词汇表与实施相矛盾,则实施将是正确的来源。
      • 区分“生成器”和“生成器迭代器”是回答这个问题的重要部分。词汇表是最精确的可用来源。术语表与实施不矛盾。您将迭代器和协程与生成器混为一谈,但它们并不相同。
      猜你喜欢
      • 2017-01-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-07-24
      • 1970-01-01
      相关资源
      最近更新 更多