![自学Python:编程基础、科学计算及数据分析](https://wfqqreader-1252317822.image.myqcloud.com/cover/254/34659254/b_34659254.jpg)
3.4 上下文管理器与with语句
之前在介绍文件读写的时候我们说过,当一个文件打开的时候,可能会遇到不正常关闭的问题,这样可能会影响我们读写文件。
不仅是文件,当我们处理资源的时候都可能会遇到这样的问题,这些资源包括文件、线程、数据库、网络连接等等。
写入文件时,如果文件没有被正常关闭,会导致某些内容没有来得及写入的情况。
例如,用一个循环向文件中写入数据:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/129_03.jpg?sign=1739300920-fHrMeJCXo38MIkpiLqUu8n4FACQT6sby-0-2d217a38636b7eb16b69b74d0c5edfaa)
当i循环到500的时候抛出了一个ZeroDivisionError,程序中断,上一行应该执行了写入“line 500”的操作。
打开这个文件,我们可能在文件结尾会看到类似如下的情况:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/129_04.jpg?sign=1739300920-JS9lCZCTNUtQ7aNISLoTrl0OmOSdBCWj-0-61527764cad6528997ac6d4b81bc15e9)
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/130_01.jpg?sign=1739300920-7KNgPmSnkzXAIVkmeN9XUUjL4JOre1jg-0-66d2a48f0000852e4d2b8c3dd510c0b0)
对于上面的情况,可以采用try块的方式,在finally中确保文件f被正确关闭:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/130_02.jpg?sign=1739300920-so8Sbi7yxe7G1aIORfxwoTUBBXJ4kq12-0-2967098a638a7f4563b11c26b18ced1a)
finally能够保证f.close()正常执行。
打开文件会发现,文件的结尾保存的是正常的结果:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/130_03.jpg?sign=1739300920-u1SF0cJtHOT8Mu8tbtZOjpwYVz38ykvO-0-2654e83decc7e9eb9b85387cf79779b0)
资源使用的问题十分常见,都使用try块处理显得不够简洁。为此,Python提供了上下文管理器的机制来解决这个问题,它通常与关键字with一起使用。
对于上面的例子,我们用with语句调用的方式为:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/130_04.jpg?sign=1739300920-dRV5iX4Aoo8oN4P26VQyqp1nKmBtBUIU-0-d077ea9b9f1a6ce65dbfc5bbac8449d1)
这与使用try块的效果相同,但是简洁了许多。
3.4.1 上下文管理器的原理
1. 基本形式和with语句
with语句的基本用法如下:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/130_05.jpg?sign=1739300920-6fEON3Vi01rdqZQpGf9KsORP55G5VGXh-0-8fbbc4ba3802aacd769e2da05843f4d0)
其中<expression>是一个上下文管理器。
上下文管理器(Context Manager)是一个实现了.__enter__()方法和.__exit__()方法的对象。
文件对象包含这两个方法,所以是一个合法的上下文管理器,可以用在with语句中:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/131_01.jpg?sign=1739300920-ywR9prrKOnD4Jyjp6bgV3FTDempFME08-0-4b6a8ce5c2ab01149facbc969d17f658)
在with语句中,上下文管理器的.__enter__()方法会在<statements>执行前执行,而.__exit__()方法会在<statements>执行结束后执行。
任何实现了这两种方法的对象都是一个合法的上下文管理器。不过,上下文管理器的.__exit__()方法需要接受3个额外参数(加上self是四个)。
例如,我们定义这样一个上下文管理器:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/131_02.jpg?sign=1739300920-ghuKzb1RcCDE8gJ2cc4qmznZFoVRHvhr-0-91ee402863fb2422b751b6b913023042)
使用这个管理器:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/131_03.jpg?sign=1739300920-thXWm4lQKSRoULuKQQfOOBqLdL2yZhJy-0-654df81a73030d7e14589bdc43525fe5)
如果<statements>在执行过程中抛出了异常,.__exit__()方法会先被执行,然后抛出异常:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/131_04.jpg?sign=1739300920-WSNhO3FfxfqvZdAV0SanMJgR9V6ZJkzv-0-61d96a5a7f691612c12ead1782538adf)
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/132_01.jpg?sign=1739300920-Vga9vvaXMaYhoNZsQdxrjdQbf8LnFip3-0-6b4e3c2a08a7c2aa2491bda0b12a47a7)
2. 方法.__enter__()的返回值
为了在<statements>中使用文件对象,我们使用了as关键字的形式,将open()函数返回的文件对象赋给了f。事实上,as关键字只是将上下文管理器.__enter__()方法的返回值赋给了f,而文件对象的.__enter__()方法的返回值刚好是它本身:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/132_02.jpg?sign=1739300920-u5AS4GQKnsi45stETAxm7VbXXsYATBOy-0-31f3e022bab26cae4f82b8b6e05cf507)
我们修改TestManager的定义,将.__enter__()的返回值修改为字符串"My value!":
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/132_03.jpg?sign=1739300920-NsoIltMwA0WHtuKxuwYx8kZA3kaaHYlm-0-ce01f6117176319e746f780ef71a7a36)
然后用as关键字得到这个返回值:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/132_04.jpg?sign=1739300920-dL9WKgqp0khGlkOlNy8qp1G9jVlI3NfB-0-9317dafce1011bed50fdc86ca216787c)
返回这个上下文管理器本身是一种常用的设计:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/132_05.jpg?sign=1739300920-K3Cw3m5xG61CSZzyHVmY3EG4r5uYBOMS-0-9247ff2e9baf0b1ff4a62d598a950bd0)
3. 方法.__exit__()与异常处理
在定义上下文管理器时,方法.__exit__()需要接受额外的参数,这些额外参数与异常处理相关。
重新定义TestManager,将这些参数打印出来:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/133_01.jpg?sign=1739300920-hBbaK1tz13boqtnfJNO7kYfNJhS9SIMH-0-5e3b6c777593324b5420abb739062315)
没有异常时:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/133_02.jpg?sign=1739300920-uElMYVZUmvJmzcPQ6JbRcqA4wS8Mjx32-0-2fa9dcb2466d428ac66ab7ebaf4e4db2)
当运行过程中抛出异常时:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/133_03.jpg?sign=1739300920-1XHUs992UTT9478KJCV7aywSX0L0498H-0-89fcf49a3f3666b759786cb3333d24d6)
当运行出现异常时,这三个参数包含的是异常的具体信息。
在上面的例子中,我们只是简单的显示了异常的值,并没有像try-except块中的except块部分一样对异常进行处理,所以异常在执行完.__exit__()方法后被继续抛出了。
如果不想让异常继续抛出,我们只需要将.__exit__()方法的返回值设为True:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/134_01.jpg?sign=1739300920-3owY5jfiPKyA8YCe6WbiSSbCEe5hBeVZ-0-4c98a516b02ce39b58ff69082a31d206)
3.4.2 模块contextlib
Python提供了contextlib模块来方便我们使用上下文管理器。
1.contextmanager装饰器
contextlib模块提供了装饰器contextmanager来实现一个简单的“上下文管理器”:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/134_02.jpg?sign=1739300920-SObHGWhuWYNha3mAOVL00801Ws4QsbFK-0-9a4e0c6c7be600e6dae28cd3e9601b28)
contextmanager作为一个装饰器,对所修饰的函数有固定的要求:
该函数必须是一个生成器,且yield只被执行一次。在该函数中,yield之前的部分可以看成是.__enter__()方法的部分,yield返回的值可以看成是.__enter__()方法返回的值,yield之后的部分可以看成是.__exit__()方法的部分。
函数test_manager()返回了一个上下文管理器:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/134_03.jpg?sign=1739300920-GZMlKOMrf6ZwSC2ZQq2Cqb0NNiCHGmDN-0-229716d0a3e380fe907d3589dac88961)
使用yield的返回值:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/135_01.jpg?sign=1739300920-p3xhuvUUnwsAeArtAmlfxE928wbkWLak-0-4eaa0f5e219695dda36c2bd37e8fa8bf)
不过,这样定义出来的“上下文管理器”并不能在出错的时候保证后续处理被执行:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/135_02.jpg?sign=1739300920-45DO76kaZEUBkw7whehlWEWaUjuTRfd0-0-de49fea60b34b276fc0338af2a5959ea)
使用contextmanager构造的上下文管理器时,如果抛出异常,那么这个异常会在yield的地方重新被抛出,我们可以使用try块的形式对yield的部分进行处理。此外,为了保证对应.__exit__()的部分始终被执行,我们需要将yield后面的部分放入finally块中:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/135_03.jpg?sign=1739300920-gT8jIc6dwcoZc8FR5CqjonKDSJQDWOWa-0-a22e81f46e77b0764f693114c309644d)
2.closing函数
contextlib模块中常用的还有closing()函数。该函数接受一个对象,返回一个确保该对象的.close()方法被调用的上下文管理器,它相当于这样的一个函数:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/136_01.jpg?sign=1739300920-NNj9akZLNaX8sVjpbiGJs1hUt2gnZ6pS-0-5ccacc4faa9aa602e40a13b6be553a52)
使用这种结构,我们能确保<object>的.close()方法最终被调用了,比如打开的网页:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/136_02.jpg?sign=1739300920-ioCpjXz36bq6YucPhqKijrdEd6dRFGNc-0-9f4c5de976ff048d6708f70fac9004fa)