Python迭代器生成器和协程


纸上得来终觉浅,绝知此事要躬行。

Python迭代器生成器和协程


1. 迭代器

在 Python 这门语言中,迭代器毫无疑问是最有用的特性之一,所以为了更好的理解生成器,我们就需要好好的从基础开始说起。

[1] 可迭代 - Iterable

  • 迭代器协议是指对象需要提供next方法,它要么返回迭代中的下一项,要么就引起一个StopIteration异常,以终止迭代。
  • Python中任意的对象,只要它定义了可以返回一个迭代器的__iter__方法,或者支持下标索引的__getitem__方法,那么它就是一个可迭代对象。
  • 需要我们注意的就是,可迭代的不等同于迭代器,所以只定义了__iter__或者__getitem__方法的对象并不是迭代器。

Python迭代器生成器和协程

In [1]: L1 = [1, 2, 3]

# 列表定义了__iter__这个方法,但是它并不是一个迭代器
In [2]: L1.__iter__
Out[2]: <method-wrapper '__iter__' of list object at 0x104c06d48>

In [3]: next(l)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-3-cdc8a39da60d> in <module>()
----> 1 next(l)

NameError: name 'l' is not defined
# 随意需要手动将列表转化为可迭代对象
In [4]: L2 = iter(L1)

In [5]: next(L2)
Out[5]: 1

In [6]: next(L2)
Out[6]: 2

In [7]:L2.__next__()
Out[7]: 3

In [8]: next(L2)
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-8-632a566062d6> in <module>()
----> 1 next(L2)

StopIteration:

# 可以看到L2的类型就是迭代器
In [9]: L2
Out[9]: <list_iterator at 0x104cb3470>

# 而L1却只是一个普通的列表对象
In [10]: L1
Out[10]: [1, 2, 3]

[2] 迭代器 - Iterators

  • 实现了__iter__next方法的对象就是迭代器,其中,__iter__方法返回迭代器对象本身,next方法返回容器的下一个元素,在没有后续元素时抛出StopIteration异常。
  • 需要注意的是,在Python2中要定义的next方法名字不同,应该使用__next__方法表示,带了双下划线。
  • 虽然列表和字典本身已经实现了__iter__的可迭代,但它们的输出结果都是一次性获取值,而迭代器是按需进行生成的。如果数量集很大的话,就会占用很大的内存消耗,且使用迭代器可进行定制化、更方便、更优雅,所以使用场景很多。

Python迭代器生成器和协程

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

    def __iter__(self):
        return self

    def next(self):
        fib = self.a
        if fib > self.max:
            raise StopIteration
        self.a, self.b = self.b, self.a + self.b
        return fib
In [16]: from iterable import Fib

In [17]: f = Fib(100)

In [18]: for i in f:
    ...:     print(i)
    ...:
0
1
1
2
3
5
8
13
21
34
55
89

In [19]: list(Fib(100))
Out[19]: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

2. 生成器

生成器(Generator)是一种使用普通函数语法定义的迭代器。生成器和普通函数的区别是使用 yield,而不是 return 返回值。

生成器的含义

  • 在汉语中,yield有生成的含义。yield的语句一次返回一个结果,在每个结果的中间会挂起函数的转态,再次调用则继续执行。
  • 生成器的本身其实还是一个迭代器,只不过yield的这种写法更为表达更为简洁且表达性更强。
In [1]: def gen():
   ...:     yield 1
   ...:     yield 2
   ...:

In [2]: g = gen()

In [3]: next(g)
Out[3]: 1

In [4]: g.__next__()
Out[4]: 2

In [5]: for i in gen():
   ...:     print(i)
   ...:
1
2
  • 简单地讲,yield的作用就是把一个函数变成一个generator,带有yield的函数不再是一个普通函数,Python解释器会将其视为一个 generator,返回一个 iterable对象。
  • for循环执行时,每次循环都会执行函数内部的代码,执行到yield x时,函数就返回一个迭代值,下次迭代时,代码从yield x的下一条语句继续执行,而函数的本地变量看起来和上次中断执行前是完全一样的,于是函数继续执行,直到再次遇到yield
# 实现斐波拉切数列的生成器版本
def fab(max):
    n, a, b = 0, 0, 1
    while n < max:
        yield b
        a, b = b, a + b
        n = n + 1

生成器表达式

  • 生成器表达式类似于列表解析式和字典解析式,除了名称上的不同以外用法基本是一样的,会按需返回一个生成器,写法更为简单明了。
In [6]: g = ( i for i in range(5) if i % 2 )

In [7]: g
Out[7]: <generator object <genexpr> at 0x108451360>

In [8]: for i in g:
   ...:     print(i)
   ...:
1
3

In [9]: next(g)
---------------------------------------------------------
StopIteration           Traceback (most recent call last)
<ipython-input-9-e734f8aca5ac> in <module>()
----> 1 next(g)

StopIteration:
In [10]: g = ( i for i in range(5) if i % 2 )

In [11]: next(g)
Out[11]: 1

In [12]: g.__next__()
Out[12]: 3

使用 yield from 语法

  • yield from表达式允许一个生成器代理另一个生成器,这样就允许生成器被替换为另一个生成器,子生成器允许返回值。
  • yield返回一个生成器 , 这个生成器就是range自身,yield from也返回一个生成器,这个生成器是由range代理的。
def gen1(x):
     yield range(x)

def gen2(x):
     yield from range(x)

it1 = gen1(5)
it2 = gen2(5)

>>> print( [ x for x in it1] )
[range(0, 5)]

>>> print( [ x for x in it2] )
[0, 1, 2, 3, 4]
class Node:
    def __init__(self, value):
        self._value = value
        self._children = []

    def __repr__(self):
        return 'Node({!r})'.format(self._value)

    def add_child(self, node):
        self._children.append(node)

    def __iter__(self):
        return iter(self._children)

    def depth_first(self):
        yield self
        for c in self:
            yield from c.depth_first()

判断函数是否为生成器

  • 要注意区分fabfab(5)fab是一个generator function,而fab(5)是调用fab返回的一个generator,好比类的定义和类的实例的区别。
  • 在一个generator function中,如果没有return,则默认执行至函数完毕,如果在执行过程中return,则直接抛出StopIteration终止迭代。
>>> from inspect import isgeneratorfunction
>>> isgeneratorfunction(fab)
True
>>> import types
>>> isinstance(fab, types.GeneratorType)
False
>>> isinstance(fab(5), types.GeneratorType)
True

# fab是无法迭代的,而fab(5)是可迭代
>>> from collections import Iterable
>>> isinstance(fab, Iterable)
False
>>> isinstance(fab(5), Iterable)
True

3. 协程

协程(Coroutine)和生成器很类似,都是包含了 yield 关键字的函数。但协程中的 yield 通常都会在表达式的右侧出现。

协程的特性优点

  • 相对于多线程而言,协程的好处在于其在一个线程里面执行避免了线程之间切换带来的额外开销。而且多线程中会使用共享的资源,往往需要加锁,而协程不需要。
  • 因为协程的代码逻辑是事先写好的且可以预料到,并不会存在在同一时间多个线程去写同一个共享变量,而导致一些资源强占的问题,当然就没有必要加锁了。

Python迭代器生成器和协程

# 协程中的yield通常都会在表达式的右侧
In [1]: def coroutine():
   ...:     print('Start coroutine...')
   ...:     x = yield
   ...:     print(f'Received: {x}')
   ...:

In [2]: coro = coroutine()

In [3]: coro
Out[3]: <generator object coroutine at 0x1056d9f68>

# 通常需要使用next执行而启动协程
In [4]: next(coro)
Start coroutine...

In [5]: coro.send(1)
Received: 1
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-5-e272bd1527da> in <module>()
----> 1 coro.send(1)

StopIteration:

协程的使用方法

  • 使用send方法可以将值传递给协程,收到值之后,yield右侧的值会先返回,再将状态挂起,之后往复交替知道执行完毕抛出StopIteration错误。
In [8]: def coroutine(a):
   ...:     print(f'Start coroutine: {a}')
   ...:     b = yield a
   ...:     print(f'Received: b={b}')
   ...:     c = yield a + b
   ...:     print(f'Received: c={c}')
   ...:

In [9]: coro = coroutine(1)

In [10]: next(coro)
Start coroutine: 1
Out[10]: 1

In [11]: coro.send(2)
Received: b=2
Out[11]: 3

In [12]: coro.send(10)
Received: c=10
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-12-8e657389bc11> in <module>()
----> 1 coro.send(10)

StopIteration:

协程可以将异步的编程同步化

  • 回调函数常常是异步编程使用的一种手法,主线程发起一个任务让其自行工作,当这个任务完成之后会调用预先指定的回调函数完成后续的任务,然后返回到主线程中。这样的好处在于,主线程无须等待可以处理其他任务。但这样回调的方式,会让人不舒服且不友好。
  • 使用协程就不需要在使用回调函数了,用yield就可以改善程序的结构设计。我们使用协程就可以实现,主程序和子程序之前的交互,通过yieldsend完成数据的传递,完成相同的工作。
# 回调的示例
In [20]: def framework(logic, callback):
    ...:     s = logic()
    ...:     print(f'[FX] logic: {s}')
    ...:     print(f'[FX] do something...')
    ...:     callback(f'async: {s}')
    ...:

In [21]: def logic():
    ...:     return 'Logic'
    ...:

In [22]: def callback(s):
    ...:     print(s)
    ...:

In [23]: framework(logic, callback)
[FX] logic: Logic
[FX] do something...
async: Logic
# 协程的示例,framework是主线程,logic是协程
In [24]: def framework(logic):
    ...:     try:
    ...:         it = logic()
    ...:         s = next(it)
    ...:         print(f'[FX] logic: {s}')
    ...:         print(f'[FX] do something...')
    ...:         it.send(f'async: {s}')
    ...:     except StopIteration:
    ...:         pass
    ...:

In [25]: def logic():
    ...:     s = 'Logic'
    ...:     r = yield s
    ...:     print(r)
    ...:

In [26]: framework(logic)
[FX] logic: Logic
[FX] do something...
async: Logic

协程完成生产者和消费者

  • 简单示例协程的工作方式
# 当然在一个线程中这样写是不正确的,因为协程中使用了while True已经阻塞的当前的线程了,需要使用两个线程。但如下所示的协程,就可以在一个线程使用哈。

In [30]: def consumer():
    ...:     while True:
    ...:         value = yield
    ...:         print(f'consume: {value}')
    ...:

In [31]: def producer(c):
    ...:     for i in range(10, 13):
    ...:         c.send(i)
    ...:

# 生成一个协程
In [32]: c = consumer()

# 类似于next方法启动协程
In [33]: c.send(None)

In [34]: producer(c)
consume: 10
consume: 11
consume: 12

# 关闭协程
In [35]: c.close()
  • 协程在消费者和生产者之间切换执行
In [40]: def consumer():
    ...:     req = 'Start consumer ...'
    ...:     while True:
    ...:         value = yield req
    ...:         print(f'consumer: {value}')
    ...:         req = f'Result: {value * 2}'
    ...:

In [41]: def producer(c):
    ...:     for i in range(10, 13):
    ...:         print(f'Producing {i} ...')
    ...:         req = c.send(i)
    ...:         print(f'Consumer return: {req}')
    ...:

In [42]: c = consumer()

In [43]: c.send(None)
Start consumer ...

In [44]: producer(c)
Producing 10 ...
consumer: 10
Consumer return: Result: 20
Producing 11 ...
consumer: 11
Consumer return: Result: 22
Producing 12 ...
consumer: 12
Consumer return: Result: 24

文章作者: Escape
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Escape !
  目录