迭代器、生成器和协程

迭代器、生成器和协程

可迭代(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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,904评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,581评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,527评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,463评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,546评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,572评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,582评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,330评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,776评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,087评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,257评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,923评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,571评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,192评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,436评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,145评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352