Python中的with关键字原理详解
200-30 优惠码:NR9EZR
300-60 优惠码:HNZVTA
有效期:4.12-4.23
可叠加官方满减优惠;限当当自营(教辅教材除外)
当当小程序、APP 和网站都可使用
详细使用方法见文章:
引言
大家好,欢迎来到 Crossin的编程教室 !
在编写涉及到异常处理的资源管理时,需要使用
try/finally代码结构,这样的结构一多会导致整体代码结构 很臃肿繁琐,不易读、不美观,因此早在 Python2.6 版本就推出了with关键字。
with as语句是Pyhton提供的一种简化语法,适用于对资源进行访问的场合,确保不管使用过程中是否发生异常都会执行必要的清理操作,释放资源。今天分享的这篇文章就给大家详细讲一讲
with关键字。
with操作文件
对于系统资源如文件、数据库连接、socket 而言,应用程序打开这些资源并执行完业务逻辑之后,必须做的一件事就是要关闭(释放)该资源。
比如 Python 程序打开一个文件,往文件中写内容,写完之后,就要关闭该文件,如果不关闭会出现什么情况呢?极端情况下会出现 Too many open files 的错误,因为系统允许你打开的最大文件数量是有限的。
同样,对于数据库,如果连接数过多而没有及时关闭的话,就可能会出现 Can not connect to MySQL server Too many connections,因为数据库连接是一种非常昂贵的资源,不可能无限制的被创建。
在代码中经常会看见 with open(file) as f  对文件进行操作,其中 with 关键字到底有什么用处呢?让我们一起了解一下其底层原理。来看看如何正确关闭一个文件。
普通版:
file = "test.txt"
def fun1():
    """
    普通版
    """
    f = open(file, "w")
    f.write("hello python")
    f.close()
这样写有一个潜在的问题,如果在调用 write 的过程中,出现了异常进而导致后续代码无法继续执行,close 方法无法被正常调用,因此资源就会一直被该程序占用者释放。那么该如何改进代码呢?
进阶版:
file = "test.txt"
def fun2():
    """
    异常处理
    """
    try:
        f = open(file, "w")
        f.write("hello python")
    except Exception as e:
        print(e)
    finally:
        f.close()
改良版本的程序是对可能发生异常的代码处进行 try 捕获,使用 try/finally 语句,该语句表示如果在 try代码块中程序出现了异常,后续代码就不再执行,而直接跳转到 except 代码块。而无论如何,finally 块的代码最终都会被执行。因此,只要把 close 放在 finally 代码中,文件就一定会关闭。
高级版:
file = "test.txt"
def fun3():
    """
    with 关键字
    """
    with open(file, "w") as f:
        f.write("hello python")
一种更加简洁、优雅的方式就是用 with 关键字。open 方法的返回值赋值给变量 f,当离开 with 代码块的时候,系统会自动调用 f.close() 方法, with 的作用和使用 try/finally 语句是一样的。那么它的实现原理是什么?在讲 with 的原理前要涉及到另外一个概念,就是 上下文管理器(Context Manager)。
上下文管理器
什么是上下文?
上下文在不同的地方表示不同的含义。在编程中
context上下文其实说白了就是环境。例如 一个 APP 应用,在切换界面的时候,要保存你是在哪个屏幕跳过来的等等信息,以便你点击返回的时候能正确跳回,如果不存肯定就无法正确跳回了。
再比如线程、协程进行任务切换时,程序怎么能知道切换到另一个任务,是从头开始执行还是从中间呢?其上下文就起到作用,就是任务本身会对其环境进行保存,做到哪里了,做了多少,各种状态都会标识记录,从而形成了上下文环境,因此在切换时根据每个任务的上下文环境,继续执行,从而达到多任务。
上下文管理器
任何类实现了
__enter__()和__exit__()方法的对象都可称之为上下文管理器。上下文管理器对象可以使用
with关键字。
file = "test.txt"
def fun3():
    """
    with 关键字
    """
    with open(file, "w") as f:
        # with 代码块
        f.write("hello python")
        
    print("with 语句结束")
- __enter__(self):进入上下文管理器自动调用的方法,该方法会在 with as 代码块 执行之前执行。如果 with 语句有 as子句,那么该方法的返回值会被赋值给 as 子句后的变量;该方法可以返回多个值,因此在 as 子句后面也可以指定多个变量(多个变量必须由()括起来组成元组)。
- __exit__(self, exc_type, exc_value, exc_traceback):退出上下文管理器自动调用的方法。该方法会在 with as 代码块 执行之后执行。如果 with as 代码块成功执行结束,程序自动调用该方法,调用该方法的三个参数都为 None,如果 with as 代码块 因为 异常而中止,程序也自动调用该方法,使用- sys.exc_info得到的异常信息将作为调用该方法的参数。
基于类的上下文管理器
我们可以模拟实现一个自己的文件类,让该类实现 __enter__() 和 __exit__()方法。
"""
with关键字的实现原理
上下文管理器
"""
# 基于类实现上下文管理器
class File(object):
    def __init__(self, filename, mode):
        self.filename = filename
        self.mode = mode
        self.file = None
    def __enter__(self):
        """
        进入with as 语句的时候被with调用
        返回值作为 as 后面的变量
        """
        print("__enter__ called")
        self.file = open(self.filename, self.mode)
        return self.file
    def __exit__(self, exc_type, exc_value, exc_traceback):
        """
        离开with语句的时候被with调用
        """
        print("__exit__ called")
        print("exc_type: ", exc_type)
        print("exc_value: ", exc_value)
        print("exc_traceback: ", exc_traceback)
        self.file.close()
        print("文件关闭操作")
def main():
    with File("test.txt", "w") as f:
        print("with 代码块")
        f.write("hello python1")
        f.write("hello python2")
        # a = 1 / 0
        f.write("hello python3")
    print("with 语句结束")
if __name__ == '__main__':
    main()
__enter__() 方法返回资源对象,这里就是你将要打开的那个文件对象,__exit__()方法处理一些清除工作。
因为 File 类实现了上下文管理器,现在就可以使用 with 语句了。其运行结果如下:
__enter__ called
with 代码块
__exit__ called
exc_type:  None
exc_value:  None
exc_traceback:  None
文件关闭操作
with 语句结束把 除0异常 的代码注释去了看看结果
__enter__ called
with 代码块
__exit__ called
exc_type:  <class 'ZeroDivisionError'>
exc_value:  division by zero
exc_traceback:  <traceback object at 0x0000021F0780BCC8>
文件关闭操作
Traceback (most recent call last):
  File "C:\Users\Administrator\Desktop\pycode\withdemo.py", line 113, in <module>
    main()
  File "C:\Users\Administrator\Desktop\pycode\withdemo.py", line 106, in main
    a = 1 / 0
ZeroDivisionError: division by zero
close 方法了,由系统自动去调用,哪怕中间遇到异常 close 方法也会被调用。基于contextmanager装饰器
Python 在 contextlib 模块中还提供了一个 contextmanager 的装饰器,更进一步简化了上下文管理器的实现方式。通过 yield 将函数分割成两部分,yield 之前的语句在  __enter__  方法中执行,yield 之后的语句在 __exit__ 方法中执行。紧跟在 yield 后面的值是函数的返回值。
# 基于contextmanager装饰器
from contextlib import contextmanager
@contextmanager
def file_manager(name, mode):
    print("file_manager() called")
    try:
        f = open(name, mode)
        yield f
    finally:
        f.close()
        print("文件关闭操作")
调用
with file_manager('test.txt', 'w') as f:
    print("with 代码块")
    f.write('hello world')
    # a = 1 / 0
print("with 语句结束")
结果
file_manager() called
with 代码块
文件关闭操作
with 语句结束总结
Python 提供了 with 语法用于简化资源操作的后续清除操作,是 try/finally 的替代方法,实现原理建立在上下文管理器之上。此外,Python 还提供了一个 contextmanager 装饰器,更进一步简化上下文管理器的实现方式。基于类和基于 contextmanager 的上下文管理器,这两者在功能上是一致的。只不过,基于类的上下文管理器 更加灵活,适用于大型的系统开发,而基于 contextmanager 的上下文管理器 更加方便、简洁,适用于中小型程序。
无论使用哪一种,不要忘记在方法 __exit__() 或者是 finally 块中释放资源,这一点尤其重要。
作者:编程小灰
_往期文章推荐_
