迭代器、生成器和协程 - Go语言中文社区

迭代器、生成器和协程


迭代器、生成器和协程

可迭代(Iterable)

Python 中任意的对象, 只要定义了可以返回一个迭代器的 __iter__方法, 或者支持下标索引的 __getitem__ 方法, 那么它就是一个可迭代对象。

有些对象定义了 itergetitem 这两种方法, 但是返回的不是一个迭代器:

>>> l = [1, 2, 3]

>>> l.__iter__ # 列表定义了 __iter__ 方法, 但是不是一个迭代器
<method-wrapper '__iter__' of list object at 0x0000021E3A95E8C8>

>>> next(l)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'list' object is not an iterator

>>> l2 = iter(l) # 用 iter() 把一个列表转换为可迭代对象

>>> next(l2) # 现在可以迭代了
1

>>> next(l2)
2

>>> next(l2)
3

>>> next(l2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

>>> l2 # 列表类型的迭代器
<list_iterator object at 0x0000021E3A965208>

迭代器(Iterators)

实现了 __iter__next 方法的对象就是迭代器, 其中, __iter__ 方法返回迭代器对象本身, next 方法返回容器的下一个元素, 在没有后续元素时抛出 StopIteration 异常。

在 Python2 中是 __next__ 方法。

可迭代对象实现一个迭代器的协议, 通过这个协议, Python 的一些内置函数和语法就能方便地访问这个对象。

下面就是一个迭代器, 它定义了 iternext 方法:

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


f = Fib(100)
for i in f:
    print(i)
# 输出:
0
1
1
2
3
5
8
13
21
34
55
89

print(type(f))
# 输出: <class '__main__.Fib'>


l = list(Fib(100))
print(l)
# 输出: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

还可以通过对可迭代的对象调用内置函数 iter(), 这样就可以获得一个迭代器, 使用 next() 函数或者 next() 方法都可以获得下一个值:

>>> l = iter([1, 2, 3])

>>> next(l)
1

>>> next(l)
2

>>> l.__next__()
3

>>> l.__next__() # 列表只有 3 个元素, 所以第 4 次获取值就会抛出异常
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

生成器(Generator)

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

yield 语句一次返回一个结果, 在每个结果中间会挂起函数, 以便下次可以在离开的地方继续执行, 生成器本质上还是迭代器, 不同的是 yield 这种写法更为简洁。

def my_gen():
    yield 1
    yield 2


g = my_gen()

print(next(g))
# 输出
1

print(g.__next__())
# 输出
2

for i in my_gen():
    print(i)
# 输出:
1
2

生成器表达式

我们可以使用列表推导式类似的语法创建一个生成器的表达式:

>>> g = (i for i in range(10) if i % 2)
>>> g
<generator object <genexpr> at 0x000002E9C3CB79E8>
>>> for i in g:
...     print(i)
...
1
3
5
7
9

协程(Coroutine)

协程和生成器很类似, 都是包含了 yield 关键字的函数, 在协程中, yield 通常处于 = 右边。

当一个函数在执行的过程中被阻塞的时候, 执行了其他的事情, 当阻塞结束之后, 可以用 next 或者 send 唤起协程。

相比于多线程, 协程的好处是在一个线程里面执行, 避免了线程之间切换带来的额外的开销, 而且多线程中, 会使用共享的资源, 往往需要加锁, 而协程不需要, 因为在协程中, 代码的执行顺序在程序中是可以预见的, 是已经定义好的, 不存在多个线程同时写某一个共享变量而导致资源抢占的问题, 也就不需要加锁了。

>>> def coroutine():
...     print('Start')
...     x = yield
...     print(f'Received: {x}')
...

>>> coro = coroutine()

>>> coro # 是一个生成器, 协程
<generator object coroutine at 0x000002E9C3CB7C50>

>>> next(coro) # 启动协程
Start

>>> coro.send(10) # 10 作为 yield 的值传入
Received: 10
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

>>> coro2 = coroutine()

>>> coro2.__next__()
Start
>>> coro2.__next__()
Received: None
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

下面是一个更加复杂一点的例子:

>>> def coroutine2(a):
...     print(f'Start: {a}')
...     b = yield a
...     print(f'Received: b = {b}')
...     c = yield a + b
...     print(f'Received: c = {c}')
...

>>> coro = coroutine2(1)

>>> next(coro) # 启动协程
Start: 1
1

>>> coro.send(2) # 2 被赋值给 b
Received: b = 2
3 # send() 方法把一个值发送给了协程, 并且要产出一个值(a+b), 作为 send() 方法的返回值

>>> coro.send(10)
Received: c = 10
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

协程可以将异步的编程同步化, 回调函数是一个实现异步操作的常用方法, 主线程发起一个异步的任务, 让任务自己去工作, 当任务完成之后会通过执行预先指定的回调函数来完成后续的任务, 然后返回主线程。这种模式下, 异步任务执行的过程中主线程无需等待和阻塞, 可以继续处理其它的任务。

下面是一个回调的例子:

>>> def framework(logic, callback):
...     s = logic()
...     print(f'[FX] logic: {s}')
...     print(f'[FX] do something...')
...     callback(f'async: {s}')
...

>>> def logic():
...     return 'Logic'
...

>>> def callback(s):
...     print(s)
...

>>> framework(logic, callback)
[FX] logic: Logic
[FX] do something...
async: Logic

上面的例子中, 每次主程序调用的时候, 都要传入一个 callback(回调函数), 使用这种回调编程的方式比较不友好, 使用协程处理则可以避免传入回调, 下面是一个使用 yield 改善程序的结构设计的例子, 让回调函数放在逻辑的最后, 中间用一个 yield 隔开, 当执行之后, send 结果, 然后在 yield 中执行, 从而实现异步:

>>> 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
...

>>> def logic():
...     s = 'Logic'
...     r = yield s
...     print(r)
...

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

下面是一个使用 yield 来完成消费功能的例子:

>>> def consumer():
...     while True:
...         v = yield # 消费者中, yield 挂起, 等待生产者通过 send() 方法传递任务
...         print(f'consume: {v}')
...

>>> def producer(c): # 接收消费者这个协程为参数
...     for i in range(10, 13):
...         c.send(i)
...

>>> c = consumer()

>>> c.send(None)

>>> producer(c)
consume: 10
consume: 11
consume: 12

>>> c.close()

更直观一点的例子:

>>> def consumer():
...     r = ''
...     while True:
...         v = yield r
...         print(f'consume: {v}')
...         r = f'Result: {v * 2}'
...

>>> def producer(c):
...     for i in range(10, 13):
...         print(f'Producing... {i}')
...         r = c.send(i)
...         print(f'Consumer return: {r}')
...

>>> c = consumer()

>>> c.send(None)
''

>>> producer(c)
Producing... 10
consume: 10
Consumer return: Result: 20
Producing... 11
consume: 11
Consumer return: Result: 22
Producing... 12
consume: 12
Consumer return: Result: 24
版权声明:本文来源简书,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://www.jianshu.com/p/a5da94b95312
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-01-12 13:10:17
  • 阅读 ( 1672 )
  • 分类:

0 条评论

请先 登录 后评论

官方社群

GO教程

推荐文章

猜你喜欢