[Python/并发] Python异步编程的关键字辨析:async / await / yield

0 序

  • 本篇缘起于这段代码:
# 启动命令: uvicorn main:app

async def app(scope, receive, send): #`async def` 声明一个异步函数(协程函数)。调用它返回一个协程对象,需用 `await` 或事件循环驱动才会执行。这是 【ASGI 应用】的标准签名:`scope`(请求元数据字典)、`receive`(接收消息的异步可调用)、`send`(发送消息的异步可调用)。
    """最简 ASGI 应用"""
    assert scope['type'] == 'http' # `assert` 断言:若 `scope['type']` 不是 `'http'`,抛出 `AssertionError`。作用:【防御性校验】,确保该应用只处理 HTTP 请求(ASGI 还支持 websocket、lifespan 等类型)
    
    await send({ # `await` 等待 `send` 这个【异步可调用】完成。
        'type': 'http.response.start', # 发送 `http.response.start` 消息:状态码 200,响应头 `content-type: text/plain`。这对应 HTTP 响应的起始行 + 头部。
        'status': 200,
        'headers': [[b'content-type', b'text/plain']],
    })
    await send({ # 再次 `await send`,发送 `http.response.body` 消息,响应体为字节 `b'Hello, Uvicorn!'`。 两次 `send` 调用必须按顺序 `await`,保证先写头再写体。
        'type': 'http.response.body',
        'body': b'Hello, Uvicorn!',
    })

1 Python 异步编程的演变过程(选读)

缘起:Python的GIL(全局解释锁)

  • 由于众所周知的【Python解释器】的GIL(全局解释锁)的原因,导致其【线程】无法发挥【多核的并行计算能力】(当然,后来有了multiprocessing,可以实现【多进程并行】),显得比较鸡肋。
  • 既然在GIL之下,同一时刻只能有一个线程在运行,那么对于【CPU密集的程序】来说,线程之间的切换开销就成了拖累,而以【I/O为瓶颈的程序】正是协程所擅长的:
  • 多任务并发(非并行),每个任务在合适的时候挂起(发起I/O)和恢复(I/O结束)
  • Python中的协程机制经历了很长的一段发展历程。其大概经历了3个阶段:
  • 最初的生成器变形yield/send
  • 引入@asyncio.coroutineyield from
  • 在最近的 Python3.5 版本中引入async/await关键字

从yield说起

  • 先看一段普通的计算斐波那契续列的代码:
def old_fib(n):
    res = [0] * n
   index = 0
   a = 0
   b = 1
   while index < n:
        res[index] = b
      a, b = b, a + b
     index += 1
  return res
  print('-'*10 + 'test old fib' + '-'*10)

for fib_res in old_fib(20):
    print(fib_res)
  • 如果我们仅仅是需要拿到斐波那契序列的第n位,或仅仅是希望依此产生斐波那契序列,那么上面这种传统方式就会比较耗费内存。 这时,yield就派上用场了。
def fib(n):
   index = 0
   a = 0
   b = 1
   while index < n:
        yield b
        a, b = b, a + b
        index += 1
        print('-'*10 + 'test yield fib' + '-'*10)

for fib_res in fib(20):
  print(fib_res)
  

当一个函数中包含yield语句时,python会自动将其识别为一个【生成器】。这时,fib(20)并不会真正调用函数体,而是以函数体为基础生成了一个【生成器对象实例】。

yield在这里可以保留fib函数的【计算现场】,暂停fib的计算并将b返回。而将fib放入for…in循环中时,每次循环都会调用next(fib(20)),唤醒生成器,执行到下一个yield语句处,直到抛出StopIteration异常。

next(g) 函数 ≈ g.send(val)
此异常会被for循环捕获,导致跳出循环。

Send来了

  • 从上面的程序中可以看到,目前只有数据从fib(20)中通过yield流向外面的for循环;如果可以向fib(20)发送数据,那不是就可以在Python中实现【协程】了嘛。

  • 于是,Python中的生成器有了send函数,yield表达式也拥有了返回值。

  • 我们用这个特性,模拟一个额慢速斐波那契数列的计算:

def stupid_fib(n):
   index = 0
   a = 0
   b = 1
   while index < n:
     sleep_cnt = yield b
     print('let me think {0} secs'.format(sleep_cnt))
     time.sleep(sleep_cnt)
     a, b = b, a + b
     index += 1
     print('-'*10 + 'test yield send' + '-'*10)

N = 20
sfib = stupid_fib(N)
fib_res = next(sfib)
while True:
  print(fib_res)
  try:
    fib_res = sfib.send(random.uniform(0, 0.5))
   except StopIteration:
     break

其中next(sfib)相当于sfib.send(None),可以使得sfib运行至第一个yield处返回。后续的sfib.send(random.uniform(0, 0.5))则将一个随机的秒数发送给sfib,作为当前中断的yield表达式的返回值。
这样,我们可以从“主”程序控制协程计算斐波那契数列时的思考时间,【协程】可以返回给【“主”程序】计算结果,Perfect!

yield from是个什么鬼?

  • yield from用于重构生成器,简单的,可以这么使用:
def copy_fib(n):
  print('I am copy from fib')
  yield from fib(n)
  print('Copy end')
  print('-'*10 + 'test yield from' + '-'*10)

for fib_res in copy_fib(20):
 print(fib_res)

这种使用方式很简单,但远远不是yield from的全部。yield from的作用还体现可以像一个管道一样将send信息传递给内层协程,并且处理好了各种异常情况,因此,对于stupid_fib也可以这样包装和使用:

def copy_stupid_fib(n):
  print('I am copy from stupid fib')
  yield from stupid_fib(n)
  print('Copy end')
  print('-'*10 + 'test yield from and send' + '-'*10)

N = 20
csfib = copy_stupid_fib(N)
fib_res = next(csfib)
while True:
  print(fib_res)
  try:
    fib_res = csfib.send(random.uniform(0, 0.5))
  except StopIteration:
    break

如果没有yield from,这里的copy_yield_from将会特别复杂(因为要自己处理各种异常)。

asyncio.coroutine和yield from

  • yield from在asyncio模块中得以发扬光大。先看示例代码:

def smart_fib(n):
  index = 0
  a = 0
  b = 1
  while index < n:
    sleep_secs = random.uniform(0, 0.2)
    yield from asyncio.sleep(sleep_secs)
    print('Smart one think {} secs to get {}'.format(sleep_secs, b))
    a, b = b, a + b
    index += 1

def stupid_fib(n):
  index = 0
  a = 0
  b = 1
  while index < n:
    sleep_secs = random.uniform(0, 0.4)
    yield from asyncio.sleep(sleep_secs)
    print('Stupid one think {} secs to get {}'.format(sleep_secs, b))
    a, b = b, a + b
    index += 1
    if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    tasks = [
      asyncio.async(smart_fib(10)),
      asyncio.async(stupid_fib(10)),
    ]
    loop.run_until_complete(asyncio.wait(tasks))
    print('All fib finished.')
    loop.close()

asyncio是一个基于事件循环的实现异步I/O的模块

通过yield from,我们可以将协程asyncio.sleep的控制权交给事件循环,然后挂起当前协程;之后,由事件循环决定何时唤醒asyncio.sleep,接着向后执行代码。

  • 这样说可能比较抽象,好在asyncio是一个由python实现的模块,那么我们来看看asyncio.sleep中都做了些什么:
def sleep(delay, result=None, *, loop=None):
    """Coroutine that completes after a given time (in seconds)."""
    future = futures.Future(loop=loop)
    h = future._loop.call_later(delay, future._set_result_unless_cancelled, result)
    try:
        return (yield from future)
    finally:
        h.cancel()        

首先,sleep创建了一个Future对象,作为更内层的协程对象,通过yield from交给了事件循环;其次,它通过调用事件循环的call_later函数,注册了一个回调函数。

  • 通过查看Future类的源码,可以看到,Future是一个实现了 __iter__ 对象的生成器:

class Future:
    def __iter__(self):
        if not self.done():
            self._blocking = True
            yield self  
        assert self.done(), "yield from wasn't used with future"
        return self.result()  
    ...     

那么当我们的协程yield from asyncio.sleep时,事件循环其实是与Future对象建立了练习。每次事件循环调用send(None)时,其实都会传递到Future对象的__iter__函数调用;而当Future尚未执行完毕的时候,就会yield self,也就意味着暂时挂起,等待下一次send(None)的唤醒。
当我们包装一个Future对象产生一个Task对象时,在Task对象初始化中,就会调用Future的send(None),并且为Future设置好回调函数。


class Task(futures.Future):
    def _step(self, value=None, exc=None):
        try:
            if exc is not None:
                result = coro.throw(exc)
            elif value is not None:
                result = coro.send(value)
            else:
                result = next(coro)
        else:
            if isinstance(result, futures.Future):
                if result._blocking:
                    result._blocking = False
                    result.add_done_callback(self._wakeup)
    def _wakeup(self, future):
        try:
            value = future.result()
        except Exception as exc:
            self._step(None, exc)
        else:
            self._step(value, None)
        self = None
  • 预设的时间过后,事件循环将调用Future._set_result_unless_cancelled:

class Future:
    def _set_result_unless_cancelled(self, result):
        """Helper setting the result only if the future was not cancelled."""
        if self.cancelled():
            return
        self.set_result(result)
    def set_result(self, result):
        """Mark the future done and set its result.        If the future is already done when this method is called, raises        InvalidStateError.        """
        if self._state != _PENDING:
            raise InvalidStateError('{}: {!r}'.format(self._state, self))
        self._result = result
        self._state = _FINISHED
        self._schedule_callbacks()

async和await

  • 弄清楚了asyncio.coroutine和yield from之后,在Python3.5中引入的async和await就不难理解了:可以将他们理解成asyncio.coroutine/yield from的完美替身。当然,从Python设计的角度来说,async/await让协程表面上独立于生成器而存在,将细节都隐藏于asyncio模块之下,语法更清晰明了。

async def smart_fib(n):
  index = 0
  a = 0
  b = 1
  while index < n:
    sleep_secs = random.uniform(0, 0.2)
    await asyncio.sleep(sleep_secs)
    print('Smart one think {} secs to get {}'.format(sleep_secs, b))
    a, b = b, a + b
    index += 1

async def stupid_fib(n):
  index = 0
  a = 0
  b = 1
  while index < n:
    sleep_secs = random.uniform(0, 0.4)
    await asyncio.sleep(sleep_secs)
    print('Stupid one think {} secs to get {}'.format(sleep_secs, b))
    a, b = b, a + b
    index += 1

if __name__ == '__main__':
  loop = asyncio.get_event_loop()
  tasks = [
    asyncio.ensure_future(smart_fib(10)),
    asyncio.ensure_future(stupid_fib(10)),
  ]
  loop.run_until_complete(asyncio.wait(tasks))
  print('All fib finished.')
  loop.close()

想要继续弄清楚async/awaitasyncio.coroutine/yield from的区别,可以看看这篇文章

2 辨析:async / await / yield / assert (必读)

综合辨析(必读)

关键字 作用
async 异步。与 def 连用声明协程函数async def),也可用于 async for(异步迭代)、async with(异步上下文管理器)。标记该函数 / 语句可被事件循环调度,执行中【可暂停让出控制权】。
await 异步等待(而非阻塞等待)。只能在 async def 内部使用。等待一个可等待对象(协程、Future、Task)完成。执行到 await 时,【当前协程】暂停并让出【事件循环】,等结果就绪后恢复执行,从而实现并发而非阻塞。
assert 调试期断言。assert 条件, 消息 —— 条件为假时抛 AssertionError。用于变量校验和调试,不应用于运行时业务校验(python -O 优化模式assert 会被整体移除)。
yield 用于生成器函数(普通 def 内)。执行到 yield 时函数暂停并返回一个值,下次 next()send() 时从暂停处恢复。用于惰性生成序列、节省内存;yield from 可委托子生成器。
注意:yield 是同步机制,与 async/await 的【异步并发】是两套体系(async def 内不能用 yield,但可以用 async yield 构成【异步生成器】)。

async

英译:异步(async),与同步(sync)相对 => 用来【定义协程函数】,它本身不做等待、不切换任务,只是给【函数】打上【标签】

  • 英文:异步的、非同步的,与同步(sync)相对。
  • async 是 Python 的语法标记,用来定义协程函数,它本身不做等待、不切换任务,只是给函数打上标签。

Python 里两层作用

1. async def:定义协程函数(与【普通函数】相对)

async def func():
    pass
  • 加上 async def,这个函数就不再是【普通函数】,是协程函数
  • 调用 func() 不会执行函数内部代码,返回一个【协程对象】(coroutine object)
res = func()
print(type(res))  # <class 'coroutine'>

类比:写了一张【任务欠条】,并没有真正干活,要交给【事件循环驱动】。

2. 函数内部允许写 await

  • 只有在 async def 的函数体内,才可以使用 await,普通函数写 await 直接语法报错。
async def demo():
    await asyncio.sleep(1)  # ✅允许(异步等待1s)

def normal():
    await asyncio.sleep(1) # ❌语法错误 SyntaxError

记住分工:

  • async:标记函数,开门允许使用 await;只做标记,不执行、不等待
  • await:驱动、挂起、等待可等待对象完成,让出协程执行权

async ≠ 自动并发 (必读)

  • 很多人误区:写了 async 就【自动并发】了,不是!
import asyncio

async def work(n):
    await asyncio.sleep(n)   # 异步等待n秒
    print(f"sleep {n} done")

async def main():
    work(2)       # ⚠只生成了【协程对象】,丢弃,警告,没有运行
    await work(2) # 顺序等待2秒
    await work(1) # 再等待1秒,总耗时3秒,不是【并发】

asyncio.run(main())
  • 想要【并发】,必须手动包装成 【Task】,交给【事件循环调度】:
async def main():
    t1 = asyncio.create_task(work(2))
    t2 = asyncio.create_task(work(1))
    await t1
    await t2

async with语法糖:异步版本的 with 语句,专门用于异步上下文管理器 (必读)

  • async with异步版本的 with 语句,专门用于【异步上下文管理器】,作用和普通 with 类似:自动做资源的申请 / 释放,区别是里面的进入、退出操作是异步 IO。
  1. async with 只能写在 async def 异步函数内部。
  2. 对象必须实现 __aenter__ / __aexit__ 两个异步方法。
  3. 进入和退出都会自动 await,不用自己手动写。
  4. 常见场景:aiohttp 会话、异步数据库连接、异步锁 asyncio.Lock
  • 普通 with
with open("a.txt") as f:
    data = f.read()
# 离开本代码块,自动调用 `__exit__` 关闭文件(同步)

底层调用对象的 __enter__()__exit__(),都是【同步】方法。

  • async with
import aiohttp

async def demo():
    async with aiohttp.ClientSession() as session:
        resp = await session.get("https://www.baidu.com")
# 离开代码块,自动异步关闭 session

底层调用异步魔术方法

  1. __aenter__():异步进入上下文,需要 await
  2. __aexit__():异步退出上下文,需要 await

普通 with 不能调用异步的进入 / 退出逻辑,所以:必须用 async with

  • 自己写一个异步上下文管理器
import asyncio

class MyAsyncCtx:
    async def __aenter__(self):
        print("异步:打开资源")
        await asyncio.sleep(0.1)
        return "资源对象"

    async def __aexit__(self, exc_type, exc, tb): # 如果有异常,exc_type, exc, tb 会被设置。exec_type 是异常类型,exc 是异常对象,tb 是异常跟踪信息
        print(f"异步:释放资源: exc_type={exc_type}, exec={exec}, tb={tb}")
        await asyncio.sleep(0.1)

async def main():
    async with MyAsyncCtx() as obj:
        print(f"正在使用:{obj}")

asyncio.run(main())

out:

异步:打开资源
正在使用:资源对象
异步:释放资源: exc_type=None, exec=<built-in function exec>, tb=None

async 语法清单

  1. async def fn() → 协程函数,调用得到协程对象
  2. await obj → 在async函数内,等待可等待对象(【协程/Task/Future】)
  3. async with → 异步上下文管理器(异步版本 with,异步资源释放)
  4. async for → 异步迭代器,异步版本for循环

和 yield 做对比记忆

关键字 作用 产物
async def 标记【协程函数】 【协程对象】,靠 await 驱动
yield 【生成器函数】标记 【生成器对象】,靠 next() / send() 驱动

历史溯源:
Python3.4 用生成器 yield from 模拟协程
Python3.5 新增 async/await 独立语法,专门用于IO异步,和【普通生成器】解耦。

极简记忆口诀

  • **async打标签,await来驱动;
  • 只async不await,任务不会动。**

await

英译:异步等待(async wait)

特性总结

  • await 只能写在被 async 修饰的函数(协程函数)内部。

async def 函数调用不会直接执行,返回一个协程对象,必须交给【事件循环】执行(asyncio.run() / create_task)。

  • await 后面必须是可等待对象:协程、Task、Future,不能直接 await time.sleep(2),会报错。

  • await xxx挂起当前【协程】,【让出事件循环】,去干别的活;等 xxx 这个异步操作完成后,再回来继续执行后面代码。

  • ⚠️ 不是【阻塞】整个线程,只是让出当前协程的执行权,线程不卡住,事件循环可以调度其他协程运行。

  • await 只是在单线程内做并发,CPU 密集任务不会提速;CPU 密集要用 multiprocessing 多进程。

对比:普通阻塞式 vs. 异步等待式

  • time.sleep(2)阻塞整个线程,啥都干不了。
  • await asyncio.sleep(2)异步挂起,【线程空闲】,【事件循环】可以跑其他任务

  • 补充对比:await asyncio.sleep(delay)(正确示范) vs asyncio.sleep(delay)(反面示范)

asyncio.sleep(delay) 语法本身合法,但单独写这行代码毫无意义,不会产生等待效果。

  • asyncio.sleep(n) 返回一个协程对象(coroutine object),它只是一个 “任务说明书”,不会自动执行、不会休眠
import asyncio

# 只是创建了一个【协程对象】,什么都没做
c = asyncio.sleep(2)
print(type(c))  # <class 'coroutine'>

就像:def func(): pass,调用f()得到返回值;但不加 await,协程不会被调度执行

  • await asyncio.sleep(2)
  • async def函数内部使用
  • 把【协程】交给【事件循环】调度
  • 【当前协程】挂起、让出执行权,等待 2 秒;期间【事件循环】可以跑【其它协程】
  • 2 秒之后恢复继续向下执行
  • ✔真正实现【异步休眠】
  • ❌若只写 asyncio.sleep(2)
  1. 创建了 sleep 协程对象,直接丢弃,完全不执行
  2. 不会等待 2 秒,立刻打印 done
  3. Python 会抛出警告:RuntimeWarning: coroutine 'sleep' was never awaited

简单Demo

import asyncio

async def say_after(delay, msg):
    await asyncio.sleep(delay)   # 异步等待,让出CPU
    print(msg)

async def main():
    task1 = asyncio.create_task(say_after(2, "hello"))
    task2 = asyncio.create_task(say_after(1, "world"))

    await task1   # 等待task1完成,期间可以跑task2
    await task2

asyncio.run(main())

正确使用 asyncio.sleep 的方式x3种

  • 核心:

asyncio.sleep() 只是构造可等待对象;
await才是触发事件循环去等待这个对象完成。

import asyncio

async def foo():
    # 方式1:await,最常用,等待它完成
    await asyncio.sleep(1)

    # 方式2:包装成Task,后台并发执行
    task = asyncio.create_task(asyncio.sleep(1))
    await task

    # 方式3:直接await一个已创建的协程对象
    coro = asyncio.sleep(1)
    await coro

yield

英译:产出、生成、让步、交出 => 产出数据,暂停函数/交出执行权/下次接着跑

yield:动词,产出、生成、让步、交出

  • Python 里取「产出数据,交出执行权」两层含义。

Python 中含义: 把【普通函数】变成了【生成器函数】

  • yield 用来把【普通函数】变成【生成器函数】(generator)

当函数里面出现 yield,【调用函数】不会【执行函数体】,而是返回一个生成器对象

  • 2大核心行为:
  1. 产出值yield val,向外吐出一个值。
  2. 交出(暂停)执行权:函数暂停在 yield 处,保存当前函数现场(局部变量、执行位置);下次迭代再从暂停点继续往下跑。
  • return 区别:
  • return:直接结束函数,销毁现场。
  • yield:暂停,保留现场,可继续运行。

Demo1 基础用法

def gen():
    yield 1
    yield 2
    yield 3

g = gen()       # 不会执行函数体,得到生成器对象
print(next(g))  # 1,执行到第一个yield暂停
print(next(g))  # 2,从上次暂停继续
print(next(g))  # 3
# next(g)  # 再调用抛出 `StopIteration`
  • 也可以 for 循环遍历

底层:隐式调用了 next()

for item in gen(): # 迭代器协议:实现 `__iter__` + `__next__` => `next(g)` → 等价调用 `g.__next__()`:执行到下一个`yield`,返回产出值;无更多数据抛`StopIteration`。
    print(item)

其对应的伪代码逻辑:

g = gen()
while True:
    try:
        item = next(g)
        print(item)
    except StopIteration: # for 循环,会捕获`StopIteration`异常,结束循环,不向外抛出。
        break

Demo2:大文件读取典型场景(节省内存)

  • 不用一次性把全部内容读到内存,逐行生成
def read_large_file(path):
    with open(path, "r", encoding="utf-8") as f:
        for line in f:
            yield line.strip()

# 逐行拿,内存只存一行
for line in read_large_file("big.txt"):
    print(line)

yield 的进阶:表达式形式 x = yield

  • yield 不仅可以分步【产出数据】,还可以接收外部 send() 发送进来的值,实现简单协程。
def coro():
    print("start")
    x = yield 100   # 产出100;同时可以接收 send 过来的值赋值给x
    print(f"收到 x={x}")
    yield 200

c = coro()
next(c)        # 预激,运行到 yield 100,产出100
c.send(888)    # 向生成器发送888,x=888,继续运行到 yield 200

""" out:
start
收到 x=888
"""

早期 Python 的 asyncio 底层就是基于 yield from 实现协程;后来才出现语法糖 async/await

yield from语法糖

  • yield from sub = “交给 sub 来干活,我帮你把它所有值吐出去,通信也打通”。
  • yield x:产出 x 这一个值
  • yield from x:产出 x 里面全部的值
  • yield from x把 x 当作【子迭代器】,【全权委托】给【子迭代器】,自动迭代、产出它的所有值

x 可以是生成器、可迭代对象。
等价简写,不用手动写循环 + yield

def outer():
    yield from [1,2,3]

# 等价于
def outer():
    for v in [1,2,3]:
        yield v
  • 关键能力1:自动迭代子生成器
def sub():
    yield 10
    yield 20

def main():
    yield from sub()

# `for` / `list()` / `tuple()` 等【消费可迭代对象】,全部遵循【迭代器协议】,底层都是不断调用`next()`,捕获`StopIteration`结束。
list(main()) # [10,20]

和 for 循环一样,list 函数也隐含了 next 函数的调用 (list() 内部会反复调用 next())。其底层伪代码:

result = []
it = main()
while True:
    try:
        val = next(it)
        result.append(val)
    except StopIteration:
        break

list(可迭代对象):拿到【迭代器】,循环next(),捕获StopIteration停止,把所有返回值装进列表。

  • 关键能力2:透传 send ()、throw ()、返回值

普通 for+yield 只能传值;yield from会把外部send()、异常、子生成器return的返回值双向隧道转发,这是它最重要的特性。

历史:asyncio 早期就是用 yield from 实现【协程】,后来被 await 替代。

yield (同步生成器: 产出数据、暂停函数/下次再接着跑) vs await(异步等待/异步协程/async协程:挂起协程,让出事件循环等待IO) 对比 (必读)

关键字 归属 本质 作用
yield 生成器 同步生成器 暂停函数,产出数据;可做简单协程;用于迭代、惰性求值
await async协程 异步协程 挂起协程,让出事件循环等待IO,等待别的异步任务完成
  • next(迭代器)驱动迭代器向前执行,拿取下一个值;

  • for 循环内部反复调用next(),捕获StopIteration终止;

  • 生成器迭代器的一种,yield暂停执行,next()恢复执行。

  • 历史关系:

Python3.4:yield from 实现异步IO;
Python3.5:新增语法糖 async / await,专门做异步IO,和普通生成器解耦。

通俗总结

  1. yield:函数执行到这里,吐出一个数据,原地暂停,保存状态,下次接着跑
  2. 适合【惰性生成序列】,处理【超大数据】,【不一次性加载全部到内存】。
  3. 它是同步机制,不是异步IO;虽然可以模拟协程,但不等同 await

Python 的生成器及生成器函数

概念辨析:生成器 = 迭代器的一种特殊实现 (必读)

  • 迭代器 (Iterator):是一套协议。只要【对象】实现 __iter__() + __next__(),就是【迭代器】。

    • __next__():返回下一个值,耗尽抛StopIteration
  • 生成器 (Generator)迭代器的一种特殊实现

函数内yield,调用函数得到【生成器对象】;它自动实现了【迭代器协议】

结论:所有【生成器】都是【迭代器】,但【迭代器】不一定是【生成器】。


【小结】

  1. 【迭代器】是抽象协议;【生成器】是【迭代器的子类】实现,由 yield 产生。
  2. 迭代器的通用能力:被next()forlist()tuple()等消费。
  3. send/throw/close生成器的独有扩展方法,【普通迭代器】没有。
  4. yield from内部会调用这些【生成器专属方法】,完成【双向通信】。

send(value) :生成器函数(恢复生成器执行,同时向生成器内部的 yield 表达式【传入/赋予/覆盖】一个值)

概念定义(必读)

  • send(value)生成器方法,作用:恢复生成器执行,同时向生成器内部的 yield 表达式 传入一个值

next(g) 等价于 g.send(None)

关键区分

def gen():
    x = yield 100   # yield表达式:产出100;同时可以接收send进来的值赋值给x
    print(f"收到x={x}")
    yield 200
  1. 预激:必须先 next(g) / g.send(None),运行到第一个 yield 暂停。
  2. 再调用 g.send(888)
    • 888 送入当前暂停的yield处,赋值给x
    • 恢复生成器往下执行,直到遇到下一个yield,产出200并暂停
g = gen()
next(g)      # 运行到 yield 100,产出100,暂停
g.send(888)  # 打印 收到x=888;产出200,暂停

生成器函数对比: next() vs send()

  • next(g) = g.send(None) :恢复执行,传入的值固定是None
  • g.send(val):恢复执行,可以传入自定义值;首次不能send非None,会报错

只有 x = yield xxx 这种yield表达式,才能接收send函数传入的值;
单纯 yield xxx(语句形式),send传进去的值会被丢弃

yield from 与 send

  • yield from 会自动把外部的send()透传给子生成器,不用自己处理。

总结

  • next():只管唤醒生成器,送进去None
  • send(val):唤醒生成器,并且给暂停位置的yield表达式投递一个外部值,实现生成器双向通信

早期基于生成器的协程,就是靠 send() 做调度;后来被 async/await 取代。

Y 推荐文献

X 参考文献

posted @ 2026-08-25 14:44  千千寰宇  阅读(5)  评论(0)    收藏  举报