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

Python生成器与协程的使用

2023-02-113.0k 阅读

Python生成器的基础概念

在Python中,生成器(Generator)是一种特殊的迭代器,它提供了一种更高效、更灵活的方式来生成数据序列。与普通的迭代器不同,生成器不需要在内存中一次性生成整个序列,而是在需要时按需生成数据,这使得生成器在处理大型数据集或无限序列时非常有用。

生成器主要有两种创建方式:生成器表达式和生成器函数。

生成器表达式

生成器表达式类似于列表推导式,但它返回的是一个生成器对象,而不是列表。语法上,生成器表达式使用圆括号 () 而列表推导式使用方括号 []

示例代码如下:

# 列表推导式
nums_list = [i * 2 for i in range(5)]
print(nums_list)  # 输出: [0, 2, 4, 6, 8]

# 生成器表达式
nums_generator = (i * 2 for i in range(5))
print(nums_generator)  # 输出: <generator object <genexpr> at 0x7f9d879c9c50>

在上述代码中,nums_list 是一个完整的列表,占用一定的内存空间存储所有元素。而 nums_generator 是一个生成器对象,它并不会立即生成所有数据,只有在迭代它时才会逐个生成元素。

可以通过 next() 函数来逐个获取生成器中的元素:

nums_generator = (i * 2 for i in range(5))
print(next(nums_generator))  # 输出: 0
print(next(nums_generator))  # 输出: 2
print(next(nums_generator))  # 输出: 4
print(next(nums_generator))  # 输出: 6
print(next(nums_generator))  # 输出: 8
# print(next(nums_generator))  # 这一行会引发 StopIteration 异常,因为生成器已耗尽

当生成器中的所有元素都被迭代完后,再次调用 next() 会引发 StopIteration 异常。

生成器函数

生成器函数是一种特殊的函数,它使用 yield 关键字来暂停函数的执行并返回一个值。与普通函数不同,生成器函数在每次调用 yield 时会暂停执行,并保存当前的状态,下次调用 next() 时会从暂停的地方继续执行。

示例代码如下:

def simple_generator():
    yield 1
    yield 2
    yield 3

gen = simple_generator()
print(next(gen))  # 输出: 1
print(next(gen))  # 输出: 2
print(next(gen))  # 输出: 3
# print(next(gen))  # 引发 StopIteration 异常

simple_generator 函数中,每当执行到 yield 语句时,函数暂停执行并返回相应的值。下次调用 next() 时,函数从上次暂停的 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))

在上述代码中,fibonacci_generator 是一个生成器函数,它可以无限生成斐波那契数列。每次调用 next() 时,生成器只计算并返回下一个斐波那契数,而不会一次性生成整个无限序列,从而节省了大量内存。

延迟计算

生成器的延迟计算特性使得它在需要时才进行计算,而不是提前计算所有数据。这在某些场景下非常有用,比如当计算过程比较耗时,但只需要部分结果时。

例如,有一个复杂的计算函数,我们只需要前几个计算结果:

import time

def complex_calculation(x):
    time.sleep(1)  # 模拟耗时计算
    return x * x

def calculation_generator():
    for i in range(10):
        yield complex_calculation(i)

calc_gen = calculation_generator()
for _ in range(3):
    print(next(calc_gen))

在这个例子中,calculation_generator 生成器函数在每次调用 next() 时才会调用 complex_calculation 进行计算。如果使用普通的列表存储所有计算结果,就需要一次性计算所有 10 个结果,即使我们只需要前 3 个。

数据流处理

生成器在数据流处理方面也非常实用。比如读取大文件时,可以逐行读取文件内容,而不是一次性将整个文件读入内存。

示例代码如下:

def read_large_file(file_path):
    with open(file_path, 'r') as file:
        for line in file:
            yield line.strip()

file_gen = read_large_file('large_file.txt')
for line in file_gen:
    print(line)

上述代码中的 read_large_file 生成器函数逐行读取文件内容,并通过 yield 返回每一行。这样在处理大文件时,内存占用始终保持在较低水平。

生成器的高级特性

生成器的状态与恢复

生成器在暂停时会保存当前的状态,包括局部变量的值和执行位置。当再次调用 next() 时,生成器会从暂停的地方恢复执行。

def stateful_generator():
    value = 0
    while True:
        action = yield value
        if action == 'increment':
            value += 1
        elif action == 'decrement':
            value -= 1

gen = stateful_generator()
print(next(gen))  # 输出: 0
print(gen.send('increment'))  # 输出: 1
print(gen.send('decrement'))  # 输出: 0

stateful_generator 中,yield 不仅返回值,还可以接收通过 send() 方法发送的值。send() 方法会将值传递给 yield 表达式,并恢复生成器的执行。这里 action 变量接收 send() 传递的值,并根据值更新 value 变量。

生成器的链式调用

可以将多个生成器连接起来,形成一个生成器链。这样可以对数据进行一系列的处理。

def number_generator():
    for i in range(5):
        yield i

def square_generator(nums):
    for num in nums:
        yield num * num

def double_generator(nums):
    for num in nums:
        yield num * 2

num_gen = number_generator()
square_gen = square_generator(num_gen)
double_gen = double_generator(square_gen)

for result in double_gen:
    print(result)

在上述代码中,number_generator 生成数字序列,square_generator 对传入的数字序列进行平方处理,double_generator 对平方后的结果翻倍。通过链式调用,数据依次经过这三个生成器的处理。

Python协程的基础概念

协程(Coroutine)是一种比生成器更高级的控制流形式,它允许函数暂停和恢复执行,并且可以在不同的执行点之间传递数据。在Python中,协程基于生成器进行扩展,通过 asyncawait 关键字来实现。

协程函数与协程对象

协程函数是使用 async def 定义的函数,调用协程函数并不会立即执行函数体,而是返回一个协程对象。

示例代码如下:

import asyncio

async def simple_coroutine():
    print('开始执行协程')
    await asyncio.sleep(1)  # 模拟异步操作
    print('协程执行结束')

coroutine_obj = simple_coroutine()
print(coroutine_obj)  # 输出: <coroutine object simple_coroutine at 0x7f9d879c9e40>

在上述代码中,simple_coroutine 是一个协程函数,调用它返回一个协程对象 coroutine_objawait 关键字用于暂停协程的执行,等待一个异步操作完成,这里 asyncio.sleep(1) 模拟了一个异步的睡眠操作。

运行协程

要运行协程,需要将协程对象传递给事件循环(Event Loop)。事件循环是一个管理异步任务执行的机制,它负责调度协程的执行。

import asyncio

async def simple_coroutine():
    print('开始执行协程')
    await asyncio.sleep(1)  # 模拟异步操作
    print('协程执行结束')

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

在上述代码中,asyncio.get_event_loop() 获取事件循环对象,run_until_complete() 方法将协程对象传递给事件循环并运行,直到协程执行完毕。最后,通过 loop.close() 关闭事件循环。

协程的优势与应用场景

异步I/O操作

协程在处理异步I/O操作时非常高效。在传统的同步编程中,I/O操作(如网络请求、文件读写)会阻塞线程,导致程序在等待I/O完成时无法执行其他任务。而协程可以在I/O操作等待时暂停执行,将控制权交回事件循环,事件循环可以调度其他协程执行,从而提高程序的整体效率。

例如,使用 aiohttp 库进行异步网络请求:

import aiohttp
import asyncio

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

async def main():
    urls = ['https://jsonplaceholder.typicode.com/todos/1',
            'https://jsonplaceholder.typicode.com/todos/2']
    async with aiohttp.ClientSession() as session:
        tasks = [fetch(session, url) for url in urls]
        results = await asyncio.gather(*tasks)
        print(results)

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

在上述代码中,fetch 协程函数负责发起异步网络请求并获取响应数据。main 协程函数创建多个 fetch 任务,并使用 asyncio.gather 来并行执行这些任务。通过这种方式,程序在等待网络响应时可以执行其他任务,大大提高了效率。

高并发场景

协程适用于高并发场景,因为它可以在单线程内实现并发执行。相比于多线程,协程避免了线程切换的开销和线程安全问题。

例如,模拟一个简单的高并发任务:

import asyncio

async def task_function(task_id):
    print(f'任务 {task_id} 开始')
    await asyncio.sleep(1)  # 模拟任务执行时间
    print(f'任务 {task_id} 结束')

async def main():
    tasks = [task_function(i) for i in range(5)]
    await asyncio.gather(*tasks)

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

在上述代码中,task_function 模拟了一个简单的任务,main 函数创建了5个这样的任务并使用 asyncio.gather 并行执行。这些任务在单线程内通过协程实现并发执行,提高了程序的执行效率。

协程与生成器的关系与区别

关系

协程是基于生成器进行扩展的。在Python中,协程对象本质上也是一种生成器对象,但它通过 asyncawait 关键字进行了更高级的功能扩展。协程使用 yield from 语句来暂停和恢复执行,这与生成器中 yield 的使用有一定关联。

区别

  1. 语法:生成器使用 yield 关键字,而协程使用 async def 定义函数,并使用 await 关键字暂停执行。
  2. 用途:生成器主要用于按需生成数据序列,节省内存。而协程主要用于实现异步编程,处理高并发和异步I/O操作。
  3. 执行控制:生成器通过 next() 函数或迭代来控制执行,而协程通过事件循环和 await 关键字来控制执行流程,更加灵活和强大。

例如,对比生成器和协程的代码结构:

# 生成器示例
def generator_example():
    for i in range(3):
        yield i

gen = generator_example()
for value in gen:
    print(value)

# 协程示例
import asyncio

async def coroutine_example():
    for i in range(3):
        await asyncio.sleep(1)  # 模拟异步操作
        print(i)

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

在生成器示例中,通过迭代获取生成器生成的值。而在协程示例中,通过事件循环和 await 关键字实现异步执行。

协程的高级特性

协程间通信

协程之间可以通过队列(asyncio.Queue)进行通信。队列可以在多个协程之间传递数据,实现数据的共享和协作。

示例代码如下:

import asyncio

async def producer(queue):
    for i in range(5):
        await queue.put(i)
        print(f'生产者放入数据: {i}')
        await asyncio.sleep(1)

async def consumer(queue):
    while True:
        data = await queue.get()
        print(f'消费者取出数据: {data}')
        await asyncio.sleep(2)
        queue.task_done()

async def main():
    queue = asyncio.Queue()
    producer_task = asyncio.create_task(producer(queue))
    consumer_task = asyncio.create_task(consumer(queue))

    await asyncio.gather(producer_task, consumer_task)
    await queue.join()

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

在上述代码中,producer 协程向队列中放入数据,consumer 协程从队列中取出数据。通过 asyncio.Queue 实现了生产者 - 消费者模型,协程之间通过队列进行数据传递和协作。

异常处理

在协程中,可以使用 try - except 块来处理异常。当一个协程中抛出异常时,事件循环会捕获并处理这个异常。

import asyncio

async def error_coroutine():
    try:
        await asyncio.sleep(1)
        raise ValueError('这是一个测试异常')
    except ValueError as e:
        print(f'捕获到异常: {e}')

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

error_coroutine 中,通过 try - except 块捕获并处理了 ValueError 异常。这样可以确保在协程执行过程中出现异常时,程序不会崩溃,而是可以进行适当的处理。

取消协程

可以使用 asyncio.Task 对象的 cancel() 方法来取消正在运行的协程。当协程被取消时,会引发 CancelledError 异常,协程可以捕获这个异常并进行清理操作。

import asyncio

async def long_running_coroutine():
    try:
        print('长时间运行的协程开始')
        await asyncio.sleep(5)
        print('长时间运行的协程结束')
    except asyncio.CancelledError:
        print('协程被取消,进行清理操作')

async def main():
    task = asyncio.create_task(long_running_coroutine())
    await asyncio.sleep(2)
    task.cancel()
    try:
        await task
    except asyncio.CancelledError:
        pass

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

在上述代码中,main 函数创建了一个长时间运行的协程任务 task,在运行2秒后取消这个任务。long_running_coroutine 捕获 CancelledError 异常并进行清理操作。

通过深入理解和掌握Python的生成器与协程,开发者可以在处理数据和实现异步编程时,更加高效地利用系统资源,编写出性能更优、结构更清晰的代码。无论是处理大型数据集还是构建高并发的网络应用,生成器和协程都提供了强大而灵活的工具。