Skip to content

Latest commit

 

History

History
151 lines (121 loc) · 6.49 KB

object_oriented8.md

File metadata and controls

151 lines (121 loc) · 6.49 KB

一切皆对象——Python面向对象(八):上下文管理器(上)

在我们日常程序处理过程中,总会遇到这样的情况,需要打开一个文件、与数据库建立一个连接,甚至在多线程中获取一个互斥锁等。这类情况具有相似的过程:

  1. 获取一个对象(文件对象、数据库对象、锁对象等);
  2. 在这个对象基础上做一些操作(文件读写、数据库读写、锁内容的修改等);
  3. 释放该对象(关闭文件、关闭数据库连接、释放锁等);

此外,这其中还可能存在潜在的异常需要处理(例如,文件可能不存在,数据库连接断开等)。整个过程类似开门进屋关门离开的过程,中间的操作都在屋内这个环境下进行,而门锁则是进入和离开这个环境的核心。我们将屋内的环境称作上下文,简单理解就是一个处于特定状态的一段代码(文件打开状态),而这段代码需要一个进屋(打开文件)和锁门(关闭文件)的过程。

我们以一个文件的读写为例来说明此事。一个比较鲁棒的文件读取示例如下:

filename = 'a.txt'
try:
    f = open(filename, 'r')
except FileNotFoundError:
    print(
        'File {} not exist'.format(filename)
    )
    import sys
    sys.exit(-1)
else:
    content = f.readlines()
    print(content)
finally:
    f.close()

except FileNotFoundError保证当文件不存在时,程序会正常推出而不会崩溃;else表示当文件正确打开后做的一系列操作;finally保证了在上述else过程中,不论出现了任何问题都会关闭文件,释放资源。每次文件操作都需要上面一套流程来保证程序的健壮性,看起来很是繁琐。因此,Python给出了一个更加简洁的解决方案——上下文管理器和with...as...关键字。

来看看利用上下文管理器应当怎么改写上述代码:

filename = 'a.txt'
try:
    with open(filename, 'r') as f:
        content = f.readlines()
        print(content)
except FileNotFoundError:
    print(
        'File {} not exist'.format(filename)
    )
    import sys
    sys.exit(-1)

最大的区别在于:

  1. 文件描述符fwith...as...获取;
  2. 没有了finally代码块,else部分放进了with代码段内;

这样当处理过程出现错误时,文件会被关闭吗?后面我们会知道,答案是会。

上下文管理器在Python中同样是一类对象,它们的特点是具有__enter____exit__两个特殊方法。__enter__定义了进入这个上下文时要做的一些事,而__exit__则定义了离开这个上下文时要做的事。上下文管理器需要由with...语句调用,此时解释器会先执行__enter__方法,如果__enter__有返回值,可以利用as...来接收这个返回值。当离开这个上下文时(即缩进回到了同with一级时),解释器自动执行__exit__方法。

我们再针对上面打开文件的例子来详细描述一下整个过程。首先open()函数会打开一个文件,返回一个文件描述符对象。请注意,这个文件描述符对象才是我们的上下文管理器对象,而不是open。那么with这个文件描述符对象的时候,会自动执行它的__enter__方法。实际上,文件描述符对象的__enter__方法仅仅是把对象本身返回(return self)。后面的as f接收了这个返回值(即文件描述符对象本身),并将其绑定到标识符f上,这样,在上下文中间的代码就可以使用这个f。当使用完毕后,离开上下文,自动执行__exit__方法。这个方法做的工作会复杂一些。首先它会调用文件描述符的close方法来关闭它(这就是为什么我们不需要手动写一个finally语句来关闭它);其次,它还会处理过程中出现的异常,处理不了的异常还会重新向外层抛出(所以我们在外层包了一个try...except...语句)。

我们来自定义一个上下文管理器来熟悉一下整套流程。正如前面说的,上下文管理器是个对象,它有__enter____exit__两个方法。需要注意一点的是,__exit__需要接收几个参数。我们先利用*_来忽略这些参数,另外它的返回值必须是布尔型的,来表示其中的异常是否需要再向外层抛出:

class Context:
    def __enter__(self):
        print('In enter')
        
   	def __exit__(self, *_):
        print('In exit')
        return True

with Context():
    print('In context')
print('Out of context')
# In enter
# In context
# In exit
# Out of context

根据打印结果我们可以看到上下文管理器的流程。我们来实现一个简易的open对象:

文件a.txt内容:

欢迎关注 微信公众号: 它不只是Python

class OpenFile:
    def __init__(self, name, mod):
        self.f = open(name, mod)
    def __enter__(self):
        return self.f
    def __exit__(self, *_):
        self.f.close()
        print('File is closed automatically	')
        return True
    
filename = 'a.txt'
with OpenFile(filename, 'r') as f:
    for line in f:
        print(line)

# 欢迎关注
#
# 微信公众号:
#
# 它不只是Python
# File is closed automatically
with open(filename, 'r') as f:
    for line in f:
        print(line)
# 欢迎关注
#
# 微信公众号:
#
# 它不只是Python

是不是完全一致?

下面我们再尝试把处理文件不存在的异常也放进管理器中来进一步简化:

class OpenFile:
    def __init__(self, name, mod):
        self.f = None
        self.err = None
        try:
            self.f = open(name, mod)
        except FileNotFoundError as e:
            print('File not exits')
            self.err = e
    def __enter__(self):
        return (self.f, self.err)
    def __exit__(self, *_):
        if self.f:
            self.f.close()
        return True

filename = 'ab.txt'
with OpenFile(filename, 'r') as (f, err):
    if not err:
        for line in f:
            print(line)

# File not exits

这里如果文件不存在,我们将异常也通过__enter__返回出来,便可以利用一个if语句来替代try...except...。 关于上下文管理器的异常处理问题和标准库支持将放在系列的下期讲解。