with
以前用with时,总是觉着这玩意就是用来管理IO的,后来发现在很多其他地方也用到了,而且和IO操作无关,于是我便认识到我对这个关键字的理解产生的偏差,所以这里重新记录一下我对这几个关键字的学习
举个例子:
class Test:
def __init__(self, data) -> None:
self.data = data
def __enter__(self):
print("现在进入了区域")
return 123456
def __exit__(self, *keywords):
print("现在退出了区域")
with Test(22) as t:
print("现在在区域中, {}".format(t))
"""
现在进入了区域
现在在区域中, 123456
现在退出了区域
"""
- 在使用with后面跟着某个类时,会调用其中的__enter__方法,并且把对应的返回丢给as后面的变量
- 在退出with时,会调用__exit__函数
这样我们就能理解为什么他可以管理IO了,我们可以这样来看
from io import TextIOWrapper
class FileManger:
def __init__(self, *keywords, **args) -> None:
self.keywords = keywords
self.args = args
def __enter__(self) -> TextIOWrapper:
self.file = open(*self.keywords, **self.args)
return self.file
def __exit__(self, *keywords):
self.file.close()
with FileManger('./user.txt', encoding = 'utf-8', mode='r') as f:
print(f.read())
这样我们就实现了文件的自动关闭功能
协程
我们知道在操作系统内,有进程和线程,其中一个进程可以有多个线程,进程内的线程并发的运行。那么协程又是什么?难道是线程的继续的细分么?
其实并不是,协程是一种编程技术,并不是操作系统上的一个运行的实体,来看下面两个伪代码。
def fun1():
处理数据
IO请求
处理数据
def fun2():
处理数据
IO请求
处理数据
fun1()
fun2()
我们知道这是一个串行执行的代码,在fun1进行IO请求时就会发生阻塞,此时主线程就会停在IO请求的代码那里等待IO结束。结束后在处理数据,结束fun1,然后执行fun2。
于是我们就在想,能不能在执行fun1被IO阻塞时,去执行fun2呢?这样就可以最大限度的利用处理器。
于是我们就想到了利用多线程技术,把fun1和fun2都单独的开一个线程然后执行。
但这样的也有问题:
操作系统需要对线程进行调度切换,这会浪费时间
那么我们能不能在一个线程内实现这种技术呢?这是当然可以的,这就是协程。他可以实现代码内部不同代码块异步的执行。
asyncio
Python在3.x很早的版本就开始支持协程了,而asyncio就是一个官方的用于协程的库。而async和await关键字就是协程技术的配套关键字。
我们首先需要了解事件循环,来看下面的伪代码:
任务列表 = [Task1, Task2, Task3,...]
while True:
for task in 任务列表:
if task 已完成:
删除task
elif task 可执行 and 非阻塞:
执行task
if 任务列表为空
return
假设我们有多个任务,我们把其放到任务列表中,就会发发生上述的事。
- 每个任务都会被按照次序执行
- 如果遇到IO等阻塞操作,则会跳过当前的task
- 如果完成了,就移出
- 如果全部完成就结束
可以把上面的fun1和fun2当成两个task,当执行fun1时可以直接执行,然后IO阻塞就会跳过当前fun1,执行task2。不停地反复循环,直到两个任务都结束。
这样,在fun1被阻塞时,我们就可以腾出时间执行task2了。所以这里有一个关键的数据结构就是这个任务列表,在这个任务列表中的任务会被协程的执行。
async&await
async可以用于加载函数面前,使得这个函数变成一个协程函数(就是可以加到上面的任务列表中)。
而await后面加的是要等待的操作,在加入await后如果发生类似于IO阻塞的情况,此时就会主动地切换到其它的task执行。
举个例子:
import asyncio
async def fun1():
print("fun1处理数据")
await asyncio.sleep(1)
print("fun1处理完成")
async def fun2():
print("fun2处理数据")
await asyncio.sleep(1)
print("fun2处理完成")
task_list = [
fun1(),
fun2(),
]
asyncio.run(asyncio.wait(task_list))
"""
fun2处理数据
fun1处理数据
fun2处理完成
fun1处理完成
"""
- 给fun1和fun2添加了async关键字,使其变成了协程函数
- 使用了asyncio中的sleep函数来模拟IO阻塞
- 使用run来建立 任务列表(这里需要注意,任务列表并不是一个简单的列表)并执行。
- wait里面存放要执行的函数的列表,用于创建对应的task然后加入 任务列表中
接下来我们来改写一下上面的代码
import asyncio
async def fun1():
asyncio.create_task(fun2())
print("fun1处理数据")
await asyncio.sleep(1)
print("fun1处理完成")
async def fun2():
print("fun2处理数据")
await asyncio.sleep(1)
print("fun2处理完成")
asyncio.run(fun1())
"""
fun1处理数据
fun2处理数据
fun1处理完成
fun2处理完成
"""
我们来看看内部的细节
- asyncio.run(fun1()) 创建了一个任务列表,并且把fun1创建一个task然后加入到任务列表中
- 开始执行fun1,遇到asyncio.create_task(fun2()),于是为fun2这个协程函数创建一个task并且加入任务列表中
- 开始执行fun1中的数据处理
- 遇到IO阻塞,此时就会导致asyncio自动把当前的执行权交给 任务列表中的下一个Task,也就是fun2
- fun2处理数据,fun2阻塞,由于此时fun1也还在阻塞,于是就循环的检查他们谁阻塞结束
- fun1阻塞先结束,fun1处理完成,fun1退出 任务列表,fun2阻塞结束,fun2吹完成,fun2退出任务列表。携程执行结束
await的进一步使用
等待一个携程函数
来看下面这个代码
import asyncio
async def fun1():
print("fun1处理数据")
res = await fun2()
print(res)
print("fun1处理完成")
async def fun2():
print("fun2处理数据")
await asyncio.sleep(1)
print("fun2处理完成")
return "你好啊,fun1"
asyncio.run(fun1())
"""
fun1处理数据
fun2处理数据
fun2处理完成
你好啊,fun1
fun1处理完成
"""
这里的一个特殊之处在于我们await的是一个携程函数,而从运行结果来看,这里使用await的效果等同于直接调用该函数。
也就是这里就相当于是一个顺序的执行我们再加上一些新的改变
import asyncio
async def fun1():
print("fun1处理数据")
asyncio.create_task(fun3())
res = await fun2()
print(res)
print("fun1处理完成")
async def fun2():
print("fun2处理数据")
await asyncio.sleep(1)
print("fun2处理完成")
return "你好啊,fun1"
async def fun3():
print("fun3处理数据")
await asyncio.sleep(1)
print("fun3处理完成")
asyncio.run(fun1())
"""
fun1处理数据
fun2处理数据
fun3处理数据
fun2处理完成
你好啊,fun1
fun1处理完成
fun3处理完成
"""
可以看出这里已经有些复杂了,我们一步一步的来进行解析
- 首先fun1加入 任务列表中,然后fun1执行
- asyncio.create_task(fun3())使得fun3加入 任务列表中
- fun1等待fun2,于是进入fun2中执行
- fun2遇到阻塞,而fun1等待fun2,于是fun1和fun2都阻塞(实际上只是阻塞了fun1,因为fun2根本不在任务列表中)
- 执行fun3,然后fun3被阻塞,于是循环的判断fun1和fun3
- fun1先结束阻塞,继续执行fun2,fun2执行结束后fun1的await内容结束
- 接续执行fun1剩下的内容,fun1结束
- 接续执行fun3剩下的内容,fun3结束
等待多个携程函数
我们可以这么写
import asyncio
async def fun1():
print("fun1处理数据")
done, pending = await asyncio.wait([
fun2(),
fun2(),
])
print(done)
print("fun1处理完成")
async def fun2():
print("fun2处理数据")
await asyncio.sleep(1)
print("fun2处理完成")
return "你好啊,fun1"
asyncio.run(fun1())
"""
fun1处理数据
fun2处理数据
fun2处理数据
fun2处理完成
fun2处理完成
{<Task finished name='Task-2' coro=<fun2() done, defined at e:/ChatGLM-6B-main/test.py:24> result='你好啊,fun1'>, <Task finished name='Task-3' coro=<fun2() done, defined at e:/ChatGLM-6B-main/test.py:24> result='你好啊,fun1'>}
fun1处理完成
"""
此时可以发现,我们await的是多个携程函数,而返回值也变成了两个值,我们通常只用第一个,里面包含了携程函数的信息和携程函数的返回值
当然也可以这么写,如果不写create_task,会自动的给你创建task的
import asyncio
async def fun1():
print("fun1处理数据")
done, pending = await asyncio.wait([
asyncio.create_task(fun2(), name = 'fun1'),
asyncio.create_task(fun2(), name = 'fun2'),
])
print(done)
print("fun1处理完成")
async def fun2():
print("fun2处理数据")
await asyncio.sleep(1)
print("fun2处理完成")
return "你好啊,fun1"
asyncio.run(fun1())
上述适用create_task手动创建了task,我们以此可以设置更多的参数