1+1=10

扬长避短 vs 取长补短

Python生成器与yield小记

注意:生成器(Generator)的概念稍微有点乱(在不同的上下文可能有不同的指向):

  • 指代 生成器函数(Generator function):包含yield的函数
  • 指代 生成器迭代器(Generator iterator):包含yield的函数的返回值

或许可以这样说:?

  • 开始时,生成器函数(文档中叫做生成器),用于生成一个迭代器(就返回一个普通迭代器)。
  • 随着功能增强,生成的这个迭代器变得比普通迭代器更厉害(有了新的名字:生成器迭代器)
  • 生成器迭代器在代码中也叫做生成器

生成器(Generator Iterator)是迭代器(Iterator)的一种。它非常类似返回数组的函数——具有参数、可被调用并产生一系列的值,但是它不是构造出所有数据并一次返回,而是每次产生一个值。

生成器和yield最早于1975年出现在CLU语言中,直到2001年才出现在Python2.2中。即便如此,它在Python中也已经20多年了。

那么,面对对这个熟悉的陌生人...

从哪儿开始?

历史

  • Python 2.2:PEP 255 在Python中引入生成器的概念以及yield语句(Statement)
  • Python 2.4:PEP 289 引入生成器表达式
  • Python 2.5:PEP 342 为生成器引入send()函数,yield语句(Statement)变为yield表达式(expression)。
  • Python 3.3:PEP 380 引入 yield from。调用方、委派生成器(delegating generator)、子生成器(subgenerator)。
  • Python 3.6:PEP 525 引入支持异步的生成器,以支持async for

术语

在Python 3.4及之前,Python术语表中有两项:

  • Generator:一个返回迭代器的函数。除了包含yield语句外,和普通函数一样。
  • Generator expression:一个返回迭代器的表达式

从Python 3.5起,术语表变为三项:

  • Generator:一个返回迭代器的函数。【通常指代生成器函数(Generator function),在某些上下文系指代生成器迭代器(Generator iterator)】
  • Generator iterator:生成器函数创建(返回)的对象。
  • Generator expression:一个返回迭代器的表达式

显然,官方注意到了Generator术语的混乱,并对此进行了一定程序的认可/澄清?

或许,可以先放一张图:

generator-asyncgenerator-coroutine-classes

生成器(Generator iterator)就是迭代器

验证起来很简单:

from collections.abc import Iterator, Generator
assert issubclass(Generator, Iterator)

from collections.abc import AsyncIterator, AsyncGenerator
assert issubclass(AsyncGenerator, AsyncIterator)

有两种方式可以创建生成器迭代器:

  • 生成器函数(Generator Function)
  • 生成器表达式(Generator Expression)

接下来,我们通过具体例子,看一下:

生成器函数

一旦一个函数内部包含了yield,那么这个函数就是生成器(generator),也叫做生成器函数(generator function)。它的返回值是一个生成器迭代器(generator iterator)。

def gen():
    yield 123

from collections.abc import Iterator, Generator
assert isinstance(gen(), Iterator)
assert isinstance(gen(), Generator)

注意,此处没有使用typing中的Iterator 和 Generator【废弃】,而是使用推荐的collections.abc中的类型。

在代码中,生成器迭代器的类型是:Generator,而不是叫GeneratorIterator。再看看inspect中的这两个函数命名:

import inspect
assert inspect.isgeneratorfunction(gen)
assert inspect.isgenerator(gen())

越发感觉Generator这个名字的精神分裂,应该就是官方的这个操作制造/助长了术语的混乱。

另外,异步形式也一样:

async def agen():
    yield 123

assert isinstance(agen(), AsyncIterator)
assert isinstance(agen(), AsyncGenerator)

生成器表达式

Python中有一个推导式(comprehension)的概念,对list、set、dict 都适用:

my_list = [i for i in range(10)]
assert isinstance(my_list, list)

my_set = {i for i in range(10)}
assert isinstance(my_set, set)

my_dict = {i: i for i in range(10)}
assert isinstance(my_dict, dict)

my_tuple = tuple(i for i in range(10))
assert isinstance(my_tuple, tuple)

强迫症感觉有点受不了,tuple的语法和其他三个不一样!

当适用圆括号时,返回是生成器迭代器,而不是tuple。而这个写法也就是生成器表达式

my_gen = (i*i for i in range(10))

from collections.abc import Iterator, Generator
assert isinstance(my_gen, Iterator)
assert isinstance(my_gen, Generator)

为什么要有生成器?

PEP255对此说了好多,个人读不下去。但我想,或许一开始,只是因为手写一个迭代器(Iterator)的类太麻烦了,所以需要一个更简洁的函数方式来创建迭代器。

另外,Python手册](https://docs.python.org/3/library/stdtypes.html#generator-types) 中说:

Python’s generators provide a convenient way to implement the iterator protocol.

生成器(函数)为生成迭代器提供了一种便利的方式。

手写迭代器 vs 使用生成器(Generator Function)

依据迭代器协议(iterator protocol),创建一个迭代器,需要实现__iter____next__两个方法(或者__getitem__方法):

class FirstN:
    def __init__(self, n):
        self.n = n
        self.num = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.num < self.n:
            cur, self.num = self.num, self.num + 1
            return cur
        raise StopIteration()

for i in firstN(10):
    print(i)

确认一下:

assert isinstance(FirstN(10), Iterator)

print(FirstN.__mro__)

注意,FirstN 只是按鸭子行为实现了iterator协议,并没有从Iterator派生,而isinstance()工作正常。

而使用生成器(Generator Function),代码可简化为:

def firstN(n):
    num = 0
    while num < n:
        yield num
        num += 1

for i in firstN(10):
    print(i)

assert hasattr(firstN(10), '__iter__')
assert hasattr(firstN(10), '__next__')

前者需要先写一个类,实现协议要求的成员。后者只需要一个包含yield的函数,不用考虑底层的协议,就可以得到一个迭代器:

assert isinstance(firstN(10), Iterator)
assert isinstance(firstN(10), Generator)

话说回来,生成器迭代器的类型是Generator,而普通迭代器类型是Iterator。说明二者还是有差异的,对比一下类型看看:

生成器(Generator Iterator) 与 迭代器(Iterator) 类型

types库

在标准库 types 中,定义了 GeneratorType 和 AsyncGeneratorType 类型,具体是这样的:

def _g():
    yield 1
GeneratorType = type(_g())

async def _ag():
    yield
_ag = _ag()
AsyncGeneratorType = type(_ag)

而对于Iterator,types源码中是这么说的:

# Iterators in Python aren't a matter of type but of protocol.  A large
# and changing number of builtin types implement *some* flavor of
# iterator.  Don't check the type!  Use hasattr to check for both
# "__iter__" and "__next__" attributes instead.

typing库

在注解库 typing.py 中,定义了 Generator,AsyncGenerator,Iterator,AsyncIterator:

Generator = _alias(collections.abc.Generator, 3)
AsyncGenerator = _alias(collections.abc.AsyncGenerator, 2)
AsyncIterator = _alias(collections.abc.AsyncIterator, 1)
Iterator = _alias(collections.abc.Iterator, 1)

它们都是collections.abc 中对应类型的别名。已经废弃,新版本不建议适用。

collections.abc 库

源码位于 _collections_abc.py,从这儿可以看到 Iterable、Iterator、Generator 的派生关系:

  • Iterable:需要__iter__成员
  • Iterator:需要__iter____next__成员
  • Generator:需要__iter____next__sendthrowclose成员
class Iterable(metaclass=ABCMeta):

    __slots__ = ()

    @abstractmethod
    def __iter__(self):
        while False:
            yield None

    @classmethod
    def __subclasshook__(cls, C):
        if cls is Iterable:
            return _check_methods(C, "__iter__")
        return NotImplemented

    __class_getitem__ = classmethod(GenericAlias)


class Iterator(Iterable):

    __slots__ = ()

    @abstractmethod
    def __next__(self):
        'Return the next item from the iterator. When exhausted, raise StopIteration'
        raise StopIteration

    def __iter__(self):
        return self

    @classmethod
    def __subclasshook__(cls, C):
        if cls is Iterator:
            return _check_methods(C, '__iter__', '__next__')
        return NotImplemented
class Generator(Iterator):

    __slots__ = ()

    def __next__(self):
        """Return the next item from the generator.
        When exhausted, raise StopIteration.
        """
        return self.send(None)

    @abstractmethod
    def send(self, value):
        """Send a value into the generator.
        Return next yielded value or raise StopIteration.
        """
        raise StopIteration

    @abstractmethod
    def throw(self, typ, val=None, tb=None):
        """Raise an exception in the generator.
        Return next yielded value or raise StopIteration.
        """
        if val is None:
            if tb is None:
                raise typ
            val = typ()
        if tb is not None:
            val = val.with_traceback(tb)
        raise val

    def close(self):
        """Raise GeneratorExit inside generator.
        """
        try:
            self.throw(GeneratorExit)
        except (GeneratorExit, StopIteration):
            pass
        else:
            raise RuntimeError("generator ignored GeneratorExit")

    @classmethod
    def __subclasshook__(cls, C):
        if cls is Generator:
            return _check_methods(C, '__iter__', '__next__',
                                  'send', 'throw', 'close')
        return NotImplemented

对于异步下的三兄弟AsyncIterable、AsyncIterator、AsyncGenerator,派生关系一样的:

  • Iterable:需要__aiter__成员
  • Iterator:需要__aiter____anext__成员
  • Generator:需要__aiter____anext__asendathrowaclose成员
class AsyncIterable(metaclass=ABCMeta):

    __slots__ = ()

    @abstractmethod
    def __aiter__(self):
        return AsyncIterator()

    @classmethod
    def __subclasshook__(cls, C):
        if cls is AsyncIterable:
            return _check_methods(C, "__aiter__")
        return NotImplemented

    __class_getitem__ = classmethod(GenericAlias)


class AsyncIterator(AsyncIterable):

    __slots__ = ()

    @abstractmethod
    async def __anext__(self):
        """Return the next item or raise StopAsyncIteration when exhausted."""
        raise StopAsyncIteration

    def __aiter__(self):
        return self

    @classmethod
    def __subclasshook__(cls, C):
        if cls is AsyncIterator:
            return _check_methods(C, "__anext__", "__aiter__")
        return NotImplemented


class AsyncGenerator(AsyncIterator):

    __slots__ = ()

    async def __anext__(self):
        """Return the next item from the asynchronous generator.
        When exhausted, raise StopAsyncIteration.
        """
        return await self.asend(None)

    @abstractmethod
    async def asend(self, value):
        """Send a value into the asynchronous generator.
        Return next yielded value or raise StopAsyncIteration.
        """
        raise StopAsyncIteration

    @abstractmethod
    async def athrow(self, typ, val=None, tb=None):
        """Raise an exception in the asynchronous generator.
        Return next yielded value or raise StopAsyncIteration.
        """
        if val is None:
            if tb is None:
                raise typ
            val = typ()
        if tb is not None:
            val = val.with_traceback(tb)
        raise val

    async def aclose(self):
        """Raise GeneratorExit inside coroutine.
        """
        try:
            await self.athrow(GeneratorExit)
        except (GeneratorExit, StopAsyncIteration):
            pass
        else:
            raise RuntimeError("asynchronous generator ignored GeneratorExit")

    @classmethod
    def __subclasshook__(cls, C):
        if cls is AsyncGenerator:
            return _check_methods(C, '__aiter__', '__anext__',
                                  'asend', 'athrow', 'aclose')
        return NotImplemented

生成器(Generator Iterator)与迭代器(Iterator)

从前面可知:

  • Iterator:需要__iter____next__成员
  • Generator Iterator:需要__iter____next__sendthrowclose成员

Generator Iterator 多出来的这三个成员,就是PEP 342 的内容。Python 2.5 时引入了 send、throw和close三个成员。

换句话说,在Python 2.5之前,没有Iterator和Generator Iterator的差异,生成器函数返回的就是普通的 Iterator。

回到文章开头的类图:实际上,__iter__()__await__()返回值都是迭代器(python库中甚至某些类定义中,为了兼容,还有__iter__ = __await__的语句,比如asyncio.Future)。生成器和协程本质上,就是后者不需要用于实现迭代的__next__()成员:

生成器Generator = 迭代器Iterator + 协程Coroutine

yield

yield 似乎非常简单,它的主要工作就是以类似return的方式控制生成器函数的流程。

在Python 2.5 之前,yield 是一个语句(Statement),而不是表达式(Expression)。

生成器(函数)只能产生输出;一旦生成器(函数)被调用并生成一个迭代器,恢复执行时将没有任何办法用于传输信息到生成器函数中。

这一时期的生成器函数属于简单的生成器函数:

def firstN(maximum):
    i = 0
    while i < maximum:
        yield i
        i += 1

作为语句,yield无法返回任何值,下面的写法是非法的

val = (yield i)

Python 2.5时,yield 由语句标为表达式,所以有了后面的写法:

def firstN(maximum):
    i = 0
    while i < maximum:
        val = (yield i)
        if val is not None:
            i = val
        else:
            i += 1

从yield的角度看,它像一个”反“函数。

  • 它的参数时要输出的内容
  • 它的返回值,是要通过send() 传递进来的。否则,yield 表达式的值是 None。

In effect, a yield-expression is like an inverted function call; the argument to yield is in fact returned (yielded) from the currently executing function, and the return value of yield is the argument passed in via send().

于是,生成器迭代器来说,除了可以像普通迭代器一样,使用next()进行遍历,直到遇到StopIteration异常:

def firstN(n):
    num = 0
    while num < n:
        yield num
        num += 1

gen = firstN(3)
print(next(gen))
print(next(gen))
print(next(gen))
print(next(gen))

还可以使用send:

def firstN(maximum):
    i = 0
    while i < maximum:
        val = (yield i)
        if val is not None:
            i = val
        else:
            i += 1

gen = firstN(3)
print(next(gen))
print(gen.send(0))
print(gen.send(0))
print(next(gen))

协程(coroutine)

在生成器函数中,当yield除了可以暂停执行并返回一个值外,还可以接受参数参数(通过send())并控制后续的执行分支的时候,这个生成器函数已经属于(经典)协程的概念了。

生成器Generator = 迭代器Iterator + 协程Coroutine

除了send()外,throw()close()也都是调用者干预生成器执行流程的手段。

经典协程,在同步场景下,多个协程函数交错执行,或许可以类比,早期单核电脑上的多线程程序。

廖雪峰的官方网站给了这样的生产者消费者例子:

def consumer():
    r = ''
    while True:
        n = yield r
        if not n:
            return
        print('[CONSUMER] Consuming %s...' % n)

def produce(c):
    c.send(None)
    n = 0
    while n < 5:
        n = n + 1
        print('[PRODUCER] Producing %s...' % n)
        r = c.send(n)
    c.close()

c = consumer()
produce(c)

没有异步的加持,语法糖...

yield from

PEP 380引入术语:

  • 委派生成器(delegating generator):包含yield from 表达式的生成器函数。

  • 子生成器(sub generator):从yield from 表达式中获取的生成器。

  • 调用方:调用委派生成器的客户端代码。

没有引入这个语法前,生成器要嵌套的话:

def hello():
    for i in range(5):
        yield i

def debao():
    for i in range(10, 15):
        yield i

def mytest():
    for v in hello():
        yield v
    for v in debao():
        yield v

for v in mytest():
    print(v)

有了yield from后,mytest可以进行改写

def mytest():
    yield from hello()
    yield from debao()

yield from 表达式所做的第一件事是,调用iter(),从中获取迭代器。因此可以用于任何可迭代对象,比如:

def gen():
    yield from 'debao'
    yield from range(5)

a = list(gen())
print(a)

注意,在关键词async和await引入前,asyncio曾使用yield from实现基于生成器的协程

@asyncio.coroutine
def old_style_coroutine():
    yield from asyncio.sleep(1)

现在这种写法已经废弃。

参考

  • https://docs.python.org/3/reference/expressions.html#yield-expressions
  • https://docs.python.org/3/library/stdtypes.html#generator-types
  • https://docs.python.org/3/glossary.html#term-generator
  • https://docs.python.org/3.8/library/asyncio-task.html#generator-based-coroutines
  • https://stackoverflow.com/questions/2776829/difference-between-pythons-generators-and-iterators
  • https://en.wikipedia.org/wiki/Generator_(computer_programming)
  • https://wiki.python.org/moin/Generators
  • https://peps.python.org/pep-0255/
  • https://peps.python.org/pep-0289/
  • https://peps.python.org/pep-0342
  • https://peps.python.org/pep-0380/
  • https://peps.python.org/pep-0525/

Comments