在本教程中,我们介绍Python协程以及如何使用Python async和await关键字来创建和暂停协程。这种异步编程模型在处理大量 I/O 操作(如网络请求、文件读取等)时特别有用,可以避免程序因为等待这些操作而被阻塞。
介绍Python 协程
在编程世界中,执行每个任务需要的时间不同。有的任务可能很快,而有些任务可能需要等待外部资源,例如获取来自服务器的数据或用户输入。作为开发人员,我们经常遇到这样的情况: 我们需要同时执行多个任务,而不是在开始新的任务之前等待前面每个任务都完成。这就是异步编程发挥作用的地方,Python中的asyncio模块是实现这一目标的强大工具。
在传统的同步编程中,任务是顺序地执行,程序等待每个任务完成后再进入下一个任务。在异步编程中,我们可以使用async关键字将某些函数标记为异步,从而允许它们与其他任务并发运行。我们把async标记的函数称为协程,协程可以在运行过程中暂停执行,以实现并发运行多个协程。
要创建和暂停协程,可以使用Python async和await关键字:
- async关键字创建一个协程。
- await关键字暂停协程。
定义协程
下面定义了简单的square函数,简单返回整数的平方数:
def square(number: int) -> int:return number*number
可以将一个整数作为参数传给square()函数,来获得它的平方数:
def square(number: int) -> int:return number*numberresult = square(10)
print(result) // 100
当给函数添加async关键字时,该函数将成为协程:
async def square(number: int) -> int:return number*number
调用协程返回协程对象,然后打印结果:
async def square(number: int) -> int:return number*numberresult = square(10)
print(result)
输出如下:
<coroutine object square at 0x00000185C31E7D80>
sys:1: RuntimeWarning: coroutine 'square' was never awaited
在本例中,我们调用square()协程,将返回值赋给result变量,并将其打印出来。当你调用协程时,Python不会立即执行协程内的代码;相反,它仅返回协程对象。
输出中的第二行还显示了一条错误消息,表明从未等待协程。在下面的await部分中可以看到更多:
sys:1: RuntimeWarning: coroutine 'square' was never awaited
Go 语言的协程是轻量级的线程,称为 goroutine,Go 语言的并发模型是基于 CSP(Communicating Sequential Processes,通信顺序进程)模型。Go 语言运行时会自动调度 goroutine 在多个操作系统线程上执行。而Python 的协程基于事件循环模型,通过异步 I/O 和回调来实现并发。要运行协程,需要在事件循环中执行它。在Python 3.7之前,你必须手动创建事件循环来执行协程并关闭事件循环。
然而,从3.7版开始,asyncio库添加了一些简化事件循环管理的函数。例如,您可以使用asyncio.run()函数自动创建事件循环,运行协程并关闭它。下面的代码使用asyncio.run()函数来执行square()协程并获得结果:
import asyncioasync def square(number: int) -> int:return number*numberresult = asyncio.run(square(10))
print(result) // 100
特别要注意是,asyncio.run()被设计为asyncio程序的主要入口点。此外,asyncio.run()函数只执行一个协程,该协程可以调用程序中的其他协程。
暂停协程
await关键字暂停协程的执行。await关键字后面是对协程的调用,如下所示:
Result = await my_coroutine()
await关键字导致my_coroutine()执行并等待代码完成、返回结果。要注意,await关键字仅在协程中有效。换句话说,必须在协程中使用await关键字。
这就是为什么在上面的示例中看到的错误消息:需要在协程上使用await关键字。下面的例子展示了如何使用await关键字来暂停协程:
import asyncioasync def square(number: int) -> int:return number*numberasync def main() -> None:x = await square(10)print(f'x={x}')y = await square(5)print(f'y={y}')print(f'total={x+y}')if __name__ == '__main__':asyncio.run(main())
输出结果:
x=100
y=25
total=125
它是如何工作的。我们将重点关注main()函数 :
首先,使用await关键字调用square()协程。await关键字将暂停main()协程的执行,等待square()协程完成,并返回结果:
x = await square(10)
print(f'x={x}')
其次,使用await关键字第二次调用square()协程:
y = await square(5)
print(f' y={y}')
第三,显示总数:
print(f 'total = {x + y}')
下面的语句使用run()函数来执行main()协程并管理事件循环:
asyncio.run(main())
到目前为止,我们的程序像同步程序一样执行。它没有揭示并发模型的强大功能,但通过简单示例让你更清晰掌握异步任务创建、执行。
完整示例
以下是一个更详细的 Python 中async
和await
关键字的示例及解释:
import asyncioasync def slow_operation(name, duration):print(f"Starting {name}...")await asyncio.sleep(duration)print(f"{name} completed.")return f"{name} result"async def main():task1 = slow_operation("Task 1", 2)task2 = slow_operation("Task 2", 3)# 同时执行两个异步任务results = await asyncio.gather(task1, task2)print(results)asyncio.run(main())
async def
定义了一个异步函数。在这个例子中,slow_operation
和main
都是异步函数。slow_operation
模拟一个耗时的操作,这里使用asyncio.sleep
来暂停执行指定的时间。当遇到await asyncio.sleep(duration)
时,这个异步函数会暂停执行,将控制权交还给事件循环,让事件循环可以去执行其他的任务。main
函数中,首先创建了两个异步任务task1
和task2
,分别调用slow_operation
。然后使用asyncio.gather
来同时执行这两个任务,并等待它们全部完成。asyncio.gather
会返回一个包含所有任务结果的列表。- 最后,使用
asyncio.run(main())
来运行主异步函数,它会创建一个新的事件循环并在其中运行main
函数,直到main
函数完成后关闭事件循环。
场景实战
前面示例仅为模拟任务,下面是使用async
和await
的实际场景示例,。这个示例展示如何在等待多个网页下载的同时,不会阻塞程序的执行,可以提高程序的效率。
import asyncio
import aiohttpasync def download_page(session, url):"""异步函数,用于下载指定 URL 的网页内容。参数:- session:aiohttp 的客户端会话对象。- url:要下载的网页 URL。"""async with session.get(url) as response:if response.status == 200:return await response.text()else:return Noneasync def main():"""主异步函数,创建任务列表并等待所有任务完成。"""urls = ["https://www.example.com","https://www.example.org","https://www.example.net"]async with aiohttp.ClientSession() as session:tasks = [download_page(session, url) for url in urls]pages = await asyncio.gather(*tasks)for url, page in zip(urls, pages):if page:print(f"Downloaded {url} successfully.")else:print(f"Failed to download {url}.")if __name__ == "__main__":asyncio.run(main())
解释:
download_page
函数接受一个aiohttp
的会话对象和一个 URL 作为参数。使用async with
语句创建一个异步的 HTTP GET 请求。如果响应状态码是 200,表示请求成功,就使用await response.text()
获取网页内容并返回。如果状态码不是 200,则返回None
。main
函数中定义了要下载的网页 URL 列表。使用aiohttp.ClientSession
创建一个客户端会话对象,这个对象可以在多个请求之间复用连接以提高性能。- 通过列表推导式创建一个任务列表,每个任务都是调用
download_page
函数,传入不同的 URL 和会话对象。 - 使用
asyncio.gather
同时执行所有任务,并等待它们全部完成。asyncio.gather
会返回一个包含所有任务结果的列表,按照任务的调用顺序排列。 - 最后,遍历 URL 和对应的网页内容,如果内容不为
None
,表示下载成功,打印成功信息;否则,打印下载失败信息。
总结
- 协程是一种常规函数,它能够暂停当前正在执行的任务,去执行其他长时间运行的操作、等待结果,并从暂停点恢复。
- 使用async关键字定义协程,使用await关键字暂停协程。使用asyncio.run()函数在事件循环上自动执行协程并管理事件循环。
总之,使用async
和await
关键字可以编写异步代码,使得程序在等待某些耗时操作时可以继续执行其他任务,提高程序的效率和响应性。