Python读写文件最好的写法是「 with open(filename, mode) as f 」这种方式。 这里的with使用的就是自带的上下文管理。本篇内容介绍Python的上下文管理器(contextor)。

什么是上下文管理器

写代码的时候经常将一系列操作放在一个语句块中:

当某条件为真 – 执行这个语句块

当某条件为真 – 循环执行这个语句块

有时候我们需要在当程序在语句块中运行时保持某种状态,并且在离开语句块后结束这种状态

所以,事实上上下文管理器的任务是——代码块执行前做一些准备工作,代码块执行后做一些恢复还原状态之类的收尾工作

与Python的迭代器类似,实现了迭代协议的函数/对象即为迭代器。实现了上下文协议的函数/对象即为上下文管理器。

迭代器协议是实现了__iter__方法。上下文管理协议则是__enter____exit__。对于如下代码结构:

class Contextor:
    def __enter__(self):
        pass
 
    def __exit__(self, exc_type, exc_val, exc_tb):
        pass
 
contextor = Contextor()
 
with contextor [as var]:
    with_body

Contextor 实现了__enter____exit__这两个上下文管理器协议,当Contextor调用/实例化的时候,则创建了上下文管理器contextor。类似于实现迭代器协议类调用生成迭代器一样。

配合with语句使用的时候,上下文管理器会自动调用__enter__方法,然后进入运行时上下文环境,如果有as 从句,返回自身或另一个与运行时上下文相关的对象,值赋值给var。当with_body执行完毕退出with语句块或者with_body代码块出现异常,则会自动执行__exit__方法,并且会把对于的异常参数传递进来。如果__exit__函数返回True。则with语句代码块不会显示的抛出异常,终止程序,如果返回False,异常会被主动raise。

使用场景与详情阐述

上下文管理器经常用于一些资源的操作,需要在资源的获取与释放相关的操作,文头提到的打开关闭文件就是一个典型的例子,还有就是数据库的连接,查询,关闭处理以及线程锁等。

先看看不使用上下文管理器会出现什么问题。

示例代码:

filename = 'my_file.txt'
f = open(filename,'w') 
f.write('Hello ') 
f.write('World') 
f.close()

这种写法是不好的写法。

如果发生了意外的情况,例如写入’World’时磁盘空间不足,就会抛出异常,那么close()语句将不会被执行。

解决方案之一可以使用try-finally,如:

try:
    filename = 'my_file.txt'
    f = open(filename,'w') 
    f.write('Hello ') 
    f.write('World')
finally:
    f.close()

这个方案不够好,原因一是不够简洁,缩进不好看,二是代码不能重用,三是finally容易忘了写,根据墨菲定律,这可不是小事,人都是不可靠的。

更优秀的方案当然是使用with的上下文管理器的写法。

上下文管理器要实现__enter____exit__的特殊方法。

__enter__(self): 进入上下文管理器时调用此方法,其返回值将被放入with-as语句中as说明符指定的变量中。

__exit__(self, type, value, tb):离开上下文管理器调用此方法。如果有异常出现,type、value、tb分别为异常的类型、值和追踪信息。如果没有异常,

3个参数均设为None。此方法返回值为True或者False,分别指示被引发的异常得到了还是没有得到处理。如果返回False,引发的异常会被传递出上下文。

文件打开用上下文的实现的Demo:

class OpenFile:
    def __init__(self,filename,mode):
        self.filename = filename
        self.mode = mode
        
    def __enter__(self):
        self.f = open(self.filename,self.mode)
        return self.f  # 作为as说明符指定的变量的值
        
    def __exit__(self, *args):
        self.f.close()
        
        
with OpenFile('my_file.txt','w') as f:
    f.write('Hello ')
    f.write('World')

没有使用with只是单纯实例化OpenFile,不会进入__enter__。使用了with后才会进入。 __exit__的完整声明是def __exit__(self, type, value, tb),可以return True表明忽略异常,return False,异常会被传递出上下文。

如果语句块内部发生了异常,__exit__方法将被调用,而异常将会被重新抛出(re-raised)。当处理文件写入操作时,大部分时间你肯定不希望隐藏这些异常。而对于不希望重新抛出的异常,我们可以让__exit__方法简单的返回True来忽略语句块中发生的所有异常(大部分情况下这都不是明智之举)。

根据__exit__函数声明可知__exit__函数就能够拿到关于异常的所有信息(异常类型,异常值以及异常追踪信息),这些信息将帮助异常处理操作。以下是一个示例,只负责抛出SyntaxErrors异常。

class RaiseOnlyIfSyntaxError:
 
    def __enter__(self):
        pass
 
    def __exit__(self, exc_type, exc_val, exc_tb):
        return SyntaxError != exc_type

除了实现上下文管理协议,Python还提供了上下文库(contextlib)可以直接使用。

假设我们有一个创建数据库函数,它将返回一个数据库对象,并且在使用完之后关闭相关资源(数据库连接会话等)

我们可以像上面那样实现协议,或是通过上下文库实现:

with contextlib.closing(CreateDatabase()) as database:
    database.query()

contextlib库中使用最多的是contextlib.contextmanager。

from contextlib import contextmanager

@contextmanager
def open_file(name, mode):
    try:
        f = open(name, mode)
        yield f
    finally:
        f.close
    
with open_file(filename, mode) as f:
    f.write('Hello ')
    f.write('World')

这里最关键的是yield关键字,任何能够被yield关键词分割成两部分的函数,都能够通过装饰器装饰的上下文管理器来实现。任何在yield之前的内容都可以看做在代码块执行前的操作,而任何yield之后的操作都可以放在exit函数中。

如果你希望在上下文管理器中使用as关键字,那么就用yield返回你需要的值,它将通过as关键字赋值给新的变量。

参考资料: