MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Python中的协程与生成器

2021-04-307.4k 阅读

Python中的协程与生成器

生成器(Generators)

在Python中,生成器是一种特殊类型的迭代器。迭代器是一个对象,它包含一个可数的序列值,并且遵循迭代器协议,即它有 __iter__()__next__() 方法。生成器通过更简洁的方式来创建迭代器,它们使用 yield 关键字而不是 return 来返回值。

  1. 生成器函数 生成器函数是定义生成器的方式。它看起来和普通函数类似,但使用 yield 关键字而不是 return。每次调用 yield 时,函数暂停执行,并返回一个值给调用者。下次调用 __next__() 时,函数从暂停的地方继续执行。
def simple_generator():
    yield 1
    yield 2
    yield 3


gen = simple_generator()
print(next(gen))
print(next(gen))
print(next(gen))

在上述代码中,simple_generator 是一个生成器函数。每次调用 next(gen) 时,函数执行到下一个 yield 语句,返回相应的值。当没有更多的 yield 语句时,会引发 StopIteration 异常。

  1. 生成器表达式 除了生成器函数,还可以使用生成器表达式来创建生成器。生成器表达式与列表推导式类似,但使用圆括号而不是方括号。
gen_expression = (i * 2 for i in range(5))
print(next(gen_expression))
print(next(gen_expression))

生成器表达式在需要简单生成器的场景下非常方便,并且它们是惰性求值的,只有在需要值的时候才会计算,这在处理大数据集时可以节省内存。

  1. 生成器的优势
    • 节省内存:生成器不会一次性生成所有的值,而是按需生成。这对于处理大型数据集或无限序列非常有用。例如,生成从1到1000000的数字列表会占用大量内存,但使用生成器则不会。
# 列表方式
big_list = list(range(1000000))

# 生成器方式
big_generator = (i for i in range(1000000))
- **迭代逻辑更简洁**:生成器函数可以使用 `yield` 暂停和恢复执行,使得复杂的迭代逻辑可以通过更自然的方式表达。例如,生成斐波那契数列:
def fibonacci_generator():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b


fib_gen = fibonacci_generator()
for _ in range(10):
    print(next(fib_gen))

协程(Coroutines)

协程是一种比生成器更强大的控制流机制。它允许函数暂停、恢复执行,并且可以在暂停时接收外部传入的值。在Python中,协程的实现经历了多个阶段,从生成器为基础的协程到 async/await 语法。

  1. 基于生成器的协程 早期,Python通过生成器来实现协程。通过向生成器发送值,可以实现协作式多任务处理。
def coroutine_example():
    value = yield
    print(f"Received: {value}")


coro = coroutine_example()
next(coro)  # 启动协程,执行到第一个yield
coro.send(42)  # 向协程发送值42

在上述代码中,coroutine_example 是一个基于生成器的协程。首先调用 next(coro) 启动协程,使其执行到 yield 语句暂停。然后通过 coro.send(42) 向协程发送值,协程恢复执行并打印接收到的值。

  1. async/await 语法 Python 3.5引入了 async/await 语法,这是一种更清晰、更强大的协程实现方式。async def 定义一个异步函数,其中可以使用 await 暂停执行,等待另一个异步操作完成。
import asyncio


async def async_function():
    print("Start async function")
    await asyncio.sleep(1)
    print("End async function")


async def main():
    await async_function()


if __name__ == "__main__":
    asyncio.run(main())

在上述代码中,async_function 是一个异步函数,使用 await asyncio.sleep(1) 暂停执行1秒。main 函数也是异步函数,用于调用 async_functionasyncio.run(main()) 用于运行整个异步任务。

  1. 协程的应用场景
    • 异步I/O操作:在网络编程、文件I/O等场景中,协程可以避免阻塞主线程,提高程序的并发性能。例如,使用 aiohttp 库进行异步HTTP请求:
import aiohttp
import asyncio


async def fetch(session, url):
    async with session.get(url) as response:
        return await response.text()


async def main():
    async with aiohttp.ClientSession() as session:
        tasks = [fetch(session, 'http://example.com') for _ in range(5)]
        results = await asyncio.gather(*tasks)
        for result in results:
            print(result)


if __name__ == "__main__":
    asyncio.run(main())
- **事件驱动编程**:协程可以很好地处理事件驱动的任务,例如在游戏开发中处理用户输入、动画更新等事件。

生成器与协程的关系

  1. 技术演进 生成器是协程的基础。早期的基于生成器的协程利用了生成器可以暂停和恢复执行的特性,通过向生成器发送值来实现更复杂的控制流。而 async/await 语法则是对协程概念的进一步发展和完善,提供了更直观、更强大的异步编程模型。

  2. 功能区别

    • 生成器主要用于迭代数据:它的目的是按顺序生成一系列值,通常是为了实现迭代器协议,让数据可以被迭代访问。生成器函数使用 yield 返回值,并且一般不接收外部传入的值(除了基于生成器的协程场景)。
    • 协程更侧重于异步控制流:协程可以暂停执行,等待某个异步操作完成,并且可以在暂停时接收外部传入的值,以实现更灵活的协作式多任务处理。async/await 语法的协程通过 await 暂停并等待异步操作,并且 async def 定义的函数与普通函数在行为上有很大不同,更专注于异步执行。
  3. 使用场景区别

    • 生成器适用于:处理大数据集,需要惰性求值以节省内存的场景,以及实现简单的迭代逻辑。比如生成无限序列、逐行读取大文件等。
    • 协程适用于:处理异步I/O操作,如网络请求、文件读写等,以及需要进行事件驱动编程的场景,能够有效提高程序的并发性能,避免阻塞主线程。

深入理解协程的执行流程

  1. async def 函数的本质 当定义一个 async def 函数时,实际上创建了一个协程对象。这个对象在调用时并不会立即执行函数体中的代码,而是返回一个协程对象。
async def simple_async():
    print("Inside simple_async")


coroutine_obj = simple_async()
print(type(coroutine_obj))

在上述代码中,simple_async() 返回一个协程对象,类型为 coroutine。要真正执行协程中的代码,需要使用 await 关键字或者将其传递给事件循环。

  1. await 的作用 await 关键字用于暂停当前协程的执行,等待被等待的协程完成。当遇到 await 时,当前协程会将执行权交回给事件循环,事件循环可以调度其他可运行的协程。
import asyncio


async def task1():
    print("Task 1 start")
    await asyncio.sleep(1)
    print("Task 1 end")


async def task2():
    print("Task 2 start")
    await asyncio.sleep(2)
    print("Task 2 end")


async def main():
    await asyncio.gather(task1(), task2())


if __name__ == "__main__":
    asyncio.run(main())

在上述代码中,task1task2 中的 await asyncio.sleep 语句会暂停各自的协程执行,事件循环可以在这期间调度其他协程。asyncio.gather 用于同时运行多个协程,并等待它们全部完成。

  1. 事件循环(Event Loop) 事件循环是协程运行的核心。它负责调度协程的执行,监控I/O操作的完成,以及处理其他异步事件。在Python中,asyncio 库提供了事件循环的实现。
import asyncio


async def async_task():
    print("Async task is running")


loop = asyncio.get_event_loop()
try:
    loop.run_until_complete(async_task())
finally:
    loop.close()

在上述代码中,通过 asyncio.get_event_loop() 获取事件循环对象,然后使用 run_until_complete 方法在事件循环中运行协程。最后,在使用完事件循环后,需要调用 close 方法关闭它。

生成器的高级特性

  1. 生成器的链式调用 可以将多个生成器链接在一起,形成更复杂的数据流处理管道。例如,有一个生成器生成数字,另一个生成器对这些数字进行平方运算。
def number_generator():
    for i in range(5):
        yield i


def square_generator(gen):
    for num in gen:
        yield num ** 2


num_gen = number_generator()
square_gen = square_generator(num_gen)
for square in square_gen:
    print(square)

在上述代码中,number_generator 生成数字,square_generator 接收一个生成器作为参数,并对其生成的数字进行平方运算。通过这种方式,可以将多个简单的生成器组合成一个更复杂的生成器流水线。

  1. 生成器的状态和上下文 生成器可以保持自己的状态和上下文。例如,一个生成器可以记录已经生成了多少个值。
def stateful_generator():
    count = 0
    while True:
        yield count
        count += 1


gen = stateful_generator()
print(next(gen))
print(next(gen))
print(next(gen))

在上述代码中,stateful_generator 内部的 count 变量记录了生成的值的数量,每次调用 next(gen) 时,count 都会增加,并且生成器会记住这个状态。

协程中的异常处理

  1. 普通异常处理 在协程中,可以像在普通函数中一样使用 try-except 块来处理异常。
import asyncio


async def async_task():
    try:
        await asyncio.sleep(1)
        raise ValueError("Custom error")
    except ValueError as e:
        print(f"Caught error: {e}")


async def main():
    await async_task()


if __name__ == "__main__":
    asyncio.run(main())

在上述代码中,async_task 中的 try-except 块捕获了 ValueError 异常,并打印出错误信息。

  1. 跨协程的异常传递 当一个协程调用另一个协程时,异常可以在协程之间传递。
import asyncio


async def inner_task():
    raise ValueError("Inner task error")


async def outer_task():
    try:
        await inner_task()
    except ValueError as e:
        print(f"Caught inner task error: {e}")


async def main():
    await outer_task()


if __name__ == "__main__":
    asyncio.run(main())

在上述代码中,inner_task 抛出的 ValueError 异常被 outer_task 捕获,展示了异常在协程调用链中的传递过程。

生成器与协程在实际项目中的应用案例

  1. Web爬虫 在Web爬虫项目中,生成器可以用于生成URL列表,而协程可以用于异步地发送HTTP请求并获取页面内容。
import asyncio
import aiohttp


def url_generator():
    base_url = "http://example.com/page_{}"
    for i in range(10):
        yield base_url.format(i)


async def fetch(session, url):
    async with session.get(url) as response:
        return await response.text()


async def main():
    async with aiohttp.ClientSession() as session:
        tasks = [fetch(session, url) for url in url_generator()]
        results = await asyncio.gather(*tasks)
        for result in results:
            print(result)


if __name__ == "__main__":
    asyncio.run(main())

在上述代码中,url_generator 生成一系列URL,fetch 协程异步地获取每个URL的页面内容,通过 asyncio.gather 并发执行多个 fetch 任务。

  1. 数据处理流水线 在数据处理项目中,可以使用生成器构建数据处理流水线,而协程可以用于异步地处理数据。
import asyncio


def data_generator():
    data = [1, 2, 3, 4, 5]
    for item in data:
        yield item


async def process_data(data):
    await asyncio.sleep(1)
    return data * 2


async def data_pipeline():
    tasks = [process_data(data) for data in data_generator()]
    results = await asyncio.gather(*tasks)
    for result in results:
        print(result)


if __name__ == "__main__":
    asyncio.run(data_pipeline())

在上述代码中,data_generator 生成数据,process_data 协程异步地对数据进行处理,data_pipeline 协调整个数据处理流程。

通过深入理解生成器与协程,开发者可以更好地利用Python的异步编程能力,提高程序的性能和效率,在处理复杂任务时实现更优雅的解决方案。无论是在数据处理、网络编程还是其他领域,生成器和协程都有着广泛的应用场景。