各位观众老爷,大家好!今天咱们聊聊 Python 里的“发电机”和“传送门”—— generator
和 yield from
,看看它们是如何在协程世界里搞事情的,实现那些让人头大的复杂通信。准备好了吗?系好安全带,咱们发车啦!
第一站:什么是 Generator?它为啥这么酷?
首先,我们得明白什么是 generator
。 简单来说,generator
就像一个“迭代器工厂”,它不是一次性把所有结果都算出来丢给你,而是“按需生产”。 你要一个,它给你一个;你要两个,它给你两个。 这种“懒加载”的方式,在处理大数据或者无限序列时,简直不要太爽!
1.1 Generator 的两种创建方式
-
Generator 函数: 这是最常见的方式,函数里只要出现
yield
关键字,它就自动升级为generator
函数。def my_generator(n): i = 0 while i < n: yield i i += 1 # 创建一个 generator 对象 gen = my_generator(5) # 迭代 generator for num in gen: print(num) # 输出 0 1 2 3 4
在这个例子里,
my_generator(5)
并不会立即执行while
循环,而是返回一个generator
对象。 只有在for
循环里,每次迭代时,才会执行到yield
语句,返回一个值,并暂停执行。 下次迭代时,从上次暂停的地方继续执行。 -
Generator 表达式: 这是一种更简洁的方式,类似于列表推导式,但是用圆括号
()
包裹。gen = (x * x for x in range(5)) for num in gen: print(num) # 输出 0 1 4 9 16
generator
表达式也遵循“懒加载”原则,只有在迭代时才会计算值。
1.2 Generator 的特性
- 可迭代性:
generator
是可迭代的,可以用for
循环遍历,也可以用next()
函数获取下一个值。 - 状态保持:
generator
会记住上次执行的状态,下次迭代时从上次暂停的地方继续执行。 - 节省内存:
generator
不是一次性生成所有结果,而是按需生成,所以可以节省大量内存。 - 单向性: 默认情况下,
generator
只能向前迭代,不能后退。但是可以通过一些骚操作来实现双向通信,后面我们会讲到。
第二站:Yield 的魔力:暂停与恢复
yield
是 generator
的核心。 它的作用是:
- 返回值:
yield
会返回一个值给调用者。 - 暂停:
yield
会暂停generator
函数的执行。 - 恢复: 下次迭代时,
generator
函数会从yield
语句的下一行继续执行。
更重要的是,yield
不仅仅能返回值,还能接收值! 这就为协程通信打下了基础。
2.1 yield 的发送和接收
def echo_generator():
value = yield
print("Received:", value)
gen = echo_generator()
next(gen) # 启动 generator,执行到 yield 语句暂停
gen.send("Hello, world!") # 发送 "Hello, world!" 给 yield
# 输出:Received: Hello, world!
在这个例子中,next(gen)
用于启动 generator
,让它执行到 yield
语句暂停。 然后,gen.send("Hello, world!")
将字符串 "Hello, world!" 发送给 yield
表达式,并赋值给变量 value
。 最后,generator
函数继续执行,打印接收到的值。
2.2 Generator 的 close() 和 throw() 方法
除了 send()
方法,generator
还有 close()
和 throw()
两个重要的方法。
- close(): 用于关闭
generator
。 关闭后,再次迭代会抛出StopIteration
异常。 - throw(): 用于在
generator
内部抛出一个异常。
def error_generator():
try:
yield
except ValueError as e:
print("Caught ValueError:", e)
finally:
print("Cleaning up...")
gen = error_generator()
next(gen) # 启动 generator
gen.throw(ValueError("Something went wrong!"))
# 输出:Caught ValueError: Something went wrong!
# Cleaning up...
gen.close() # 关闭 generator
在这个例子中,gen.throw(ValueError("Something went wrong!"))
会在 generator
内部抛出一个 ValueError
异常。 try...except
块捕获了这个异常,并打印了错误信息。 最后,finally
块中的代码会被执行,进行清理工作。
第三站:Yield From:协程界的传送门
yield from
是 Python 3.3 引入的一个语法糖,它可以将一个 generator
(或者任何可迭代对象)“委托”给另一个 generator
。 简单来说,yield from
就像一个传送门,把控制权和数据流从一个 generator
传递到另一个 generator
。
3.1 Yield From 的基本用法
def sub_generator(n):
for i in range(n):
yield i
def main_generator(n):
yield from sub_generator(n)
gen = main_generator(3)
for num in gen:
print(num) # 输出 0 1 2
在这个例子中,main_generator
使用 yield from sub_generator(n)
将迭代过程委托给 sub_generator
。 相当于把 sub_generator
的代码直接“嵌入”到 main_generator
中。
3.2 Yield From 的高级用法:双向通信
yield from
最强大的地方在于它可以实现双向通信。 主调方可以通过 send()
方法向子 generator
发送数据,子 generator
也可以通过 yield
向主调方返回数据。
def sub_generator():
while True:
try:
value = yield
print("Sub-generator received:", value)
except Exception as e:
print("Sub-generator caught exception:", e)
break # 退出循环
else:
if value is None: # 如果接收到None,则退出循环
break
return "Sub-generator finished"
def main_generator():
result = yield from sub_generator()
print("Main generator received:", result)
gen = main_generator()
next(gen) # 启动 main_generator 和 sub_generator
gen.send("Hello from main")
gen.send("Another message")
gen.send(None) #告诉sub_generator退出循环
#gen.throw(Exception("Something went wrong")) # 抛出异常
try:
next(gen) # 尝试继续执行,但是会抛出StopIteration异常
except StopIteration:
print("Main generator finished")
# 输出:
# Sub-generator received: Hello from main
# Sub-generator received: Another message
# Sub-generator received: None
# Main generator received: Sub-generator finished
# Main generator finished
这个例子比较复杂,我们来一步步分析:
main_generator
使用yield from sub_generator()
将控制权交给sub_generator
。sub_generator
进入一个无限循环,等待接收数据。gen.send("Hello from main")
将字符串 "Hello from main" 发送给sub_generator
。sub_generator
接收到数据,并打印出来。gen.send(None)
发送None
给sub_generator
,sub_generator
接收到None
后退出循环,并返回 "Sub-generator finished"。yield from
表达式接收到sub_generator
的返回值,并赋值给result
。main_generator
打印接收到的返回值。main_generator
执行完毕,抛出StopIteration
异常。
3.3 Yield From 的应用场景
- 简化代码:
yield from
可以简化复杂的generator
嵌套调用,使代码更易读、易维护。 - 协程通信:
yield from
可以实现协程之间的双向通信,构建复杂的并发程序。 - 异步编程:
yield from
可以与async/await
结合使用,实现异步编程。
第四站:实战演练:构建一个简单的 Pipeline
为了更好地理解 generator
和 yield from
的用法,我们来构建一个简单的 Pipeline。 Pipeline 是一种常见的设计模式,用于将数据处理流程分解为多个阶段,每个阶段由一个独立的组件负责。
def source(items):
"""数据源,产生数据"""
for item in items:
print(f"Source: Producing {item}")
yield item
def filter_stage(items, condition):
"""过滤器,过滤掉不满足条件的数据"""
for item in items:
if condition(item):
print(f"Filter: Passing {item}")
yield item
else:
print(f"Filter: Filtering out {item}")
def transform_stage(items, func):
"""转换器,对数据进行转换"""
for item in items:
transformed_item = func(item)
print(f"Transform: Transforming {item} to {transformed_item}")
yield transformed_item
def sink(items):
"""数据汇,消费数据"""
for item in items:
print(f"Sink: Consuming {item}")
# 数据
data = [1, 2, 3, 4, 5, 6]
# 条件:只保留偶数
condition = lambda x: x % 2 == 0
# 转换函数:将数字转换为字符串
transform = lambda x: str(x)
# 构建 Pipeline
pipeline = sink(transform_stage(filter_stage(source(data), condition), transform))
# 执行 Pipeline (实际上啥都没做,因为pipeline是个生成器,需要迭代)
#for _ in pipeline:
# pass
# 使用 yield from 简化代码
def pipeline(items, condition, transform):
yield from transform_stage(filter_stage(source(items), condition), transform)
# 执行 Pipeline
for item in pipeline(data, condition, transform):
sink([item]) # sink也需要迭代
#输出结果
#Source: Producing 1
#Filter: Filtering out 1
#Source: Producing 2
#Filter: Passing 2
#Transform: Transforming 2 to 2
#Sink: Consuming 2
#Source: Producing 3
#Filter: Filtering out 3
#Source: Producing 4
#Filter: Passing 4
#Transform: Transforming 4 to 4
#Sink: Consuming 4
#Source: Producing 5
#Filter: Filtering out 5
#Source: Producing 6
#Filter: Passing 6
#Transform: Transforming 6 to 6
#Sink: Consuming 6
在这个例子中,我们定义了四个阶段:
source
:数据源,产生数据。filter_stage
:过滤器,过滤掉不满足条件的数据。transform_stage
:转换器,对数据进行转换。sink
:数据汇,消费数据。
我们使用 generator
来实现每个阶段,并使用 yield from
将它们连接起来,构建成一个 Pipeline。 这样,我们就可以清晰地看到数据的流向,以及每个阶段的处理逻辑。
第五站:Generator 和协程:异步编程的基石
generator
是协程的基础。 在 Python 3.5 引入 async/await
语法之前,generator
曾经是实现协程的主要方式。 虽然现在 async/await
更加流行,但是理解 generator
的原理,仍然有助于我们更好地理解协程的本质。
5.1 基于 Generator 的协程
def coroutine():
print("Coroutine started")
value = yield
print("Coroutine received:", value)
return "Coroutine finished"
def main():
co = coroutine()
next(co) # 启动协程
result = co.send("Hello, world!") # 发送数据给协程
print("Main received:", result)
#main() # 抛出StopIteration,因为coroutine函数已经执行完毕
#输出
#Coroutine started
#Coroutine received: Hello, world!
#Main received: Coroutine finished
在这个例子中,coroutine
函数就是一个协程。 我们使用 next(co)
启动协程,然后使用 co.send("Hello, world!")
发送数据给协程。 协程接收到数据后,打印出来,并返回 "Coroutine finished"。
5.2 Generator 和 Async/Await 的关系
async/await
实际上是 generator
的语法糖。 async
关键字将一个函数声明为协程,await
关键字用于等待一个协程执行完成。
import asyncio
async def coroutine():
print("Coroutine started")
await asyncio.sleep(1) # 模拟 IO 操作
print("Coroutine finished")
return "Coroutine result"
async def main():
print("Main started")
result = await coroutine() # 等待协程执行完成
print("Main received:", result)
print("Main finished")
#asyncio.run(main()) # 需要在事件循环中运行
#输出
#Main started
#Coroutine started
#(等待 1 秒)
#Coroutine finished
#Main received: Coroutine result
#Main finished
在这个例子中,async def coroutine()
定义了一个协程函数。 await asyncio.sleep(1)
用于等待 1 秒钟,模拟 IO 操作。 await coroutine()
用于等待 coroutine
协程执行完成,并获取其返回值。
总结:
今天我们一起学习了 Python 的 generator
和 yield from
。 它们是构建协程、实现复杂通信的利器。 希望通过今天的讲座,大家能够更好地理解它们的原理和用法,并在实际开发中灵活运用。
特性 | Generator | Yield From |
---|---|---|
作用 | 创建迭代器,按需生成数据 | 将一个 generator 委托给另一个 generator,实现双向通信 |
核心关键字 | yield |
yield from |
应用场景 | 处理大数据、无限序列、状态保持、协程 | 简化代码、协程通信、异步编程 |
优点 | 节省内存、可迭代、状态保持 | 简化代码、提高效率、实现复杂的协程通信 |
缺点 | 默认单向迭代,需要手动管理状态和异常 | 稍微复杂,需要理解其背后的原理 |
好了,今天的讲座就到这里。 感谢大家的收听! 记住,编程的路上,永远不要停止学习和探索! 咱们下回再见!