浅谈Python的上下文处理器

前言

内存泄漏

内存泄漏是指程序中已动态分配的堆内存由于某种原因使得程序未释放或无法释放,直到程序结束前,这个未被释放的内存对象一直占用系统内存空间,造成系统内存的浪费。

如今很多高级编程语言都会有自己的内存管理机制,一般不太需要使用者过多的关注内存问题。但在某些情况下,我们还是需要编写程序来关闭或者释放对象内存。

在任何一门编程语言中,文件的输入输出、数据库的连接断开等,都是很常见的资源管理操作。然而,系统中的资源是有限的,我们在写程序的时候,必须保证这些资源在使用过后得到释放,否则就会造成内存溢出问题,严重的可能会造成系统崩溃。

上下文管理器

我们还是以读写文件为例子,假如我们打开了很多个文件进行写入操作,但是没有及时关闭文件,在资源有限的情况下,就会出现内存溢出问题,如下:

for i in range(1000000000):
    f = open('a.txt', 'w')  # 打开
    f.write('Hello World')  # 写入
    # f.close()  # 关闭

为了解决以上问题,不同的编程语言都有不同的机制,而在Python中,对应的解决方法是使用上下文管理器。

上下文管理器能够帮助你自动分配并且释放资源,其中最常用的便是with语句

with open('a.txt', 'w') as f:
    f.write('Hello World')

每当我们打开文件,操作完成后这个文件便会自动关闭,这样做的好处是可以及时释放资源,防止内存泄漏。

原理与实现方法

我相信很多小伙伴在学习Python的时候应该都用过with语句,但很多人都只是停留在文件操作上的使用,并不清楚with语句的实现和上下文管理器的原理,接下来我们来探讨一下上下文管理器是如何实现的。

基本原理

概念

根据上下文管理器的协议,一个类中实现了__enter__()__exit__()两个方法,这个类就是一个上下文管理器。

基本语法

with EXPR as VAR:
	BLOCK

我们还是以上面的例子来讲解

  • 上下文表达式:with open('a.txt', 'w') as f
  • 上下文管理器:open('a.txt', 'w')
  • f是上下文管理器的资源对象

执行过程

  1. 执行上下文表达式获取上下文管理器对象
  2. 加载上下文管理器对象的__exit__()方法,备用
  3. 调用上下文管理器对象的__enter__()方法
  4. 如果with语句有指定的目标变量,将从__enter__()方法获取的相关对象赋值给目标变量
  5. 执行with语句体(上面例子的语句体为f.write('Hello'))
  6. 调用上下文管理器的__exit__()方法,如果是with语句体造成的异常退出,那异常类型、异常值、异常追踪信息将被传给__exit__(),否则,3个参数都是None

实现方法

在了解原理后,我们来尝试实现自定义的上下文管理器

基于类的上下文管理器

我们来实现一个操作文件的上下文管理器

class FileOperator:
    def __init__(self, filename, method):
        self.filename = filename
        self.method = method

    def __enter__(self):
        print('打开文件...')
        self.file = open(self.filename, self.method)
        return self.file

    def __exit__(self, exc_type, exc_val, exc_tb):
        print('关闭文件...')
        self.file.close()


if __name__ == '__main__':
    with FileOperator('a.txt', 'w') as f:
        print('写入一句话...')
        f.write('Hello Python')

执行结果如下

打开文件...
写入一句话...
关闭文件...

从上面例子可以看出,我们在编写代码时,可以将资源的获取或者连接操作放在__enter__中,而将资源的关闭写在__exit__ 中。

基于生成器的上下文管理器

在Python中,除了基于类的上下文管理器,还有基于生成器的上下文管理器。

要自定义基于生成器的上下文管理器,可以使用装饰器contextlib.contextmanager,这个装饰器的原理实际上也是实现了__enter__()__exit__()两个方法。

下面使用contextlib.contextmanager装饰器来写一个操作文件的上下文管理器

from contextlib import contextmanager


@contextmanager
def operate_file(name, method):
    print('打开文件...')
    f = open(name, method)
    yield f
    print('关闭文件...')
    f.close()


if __name__ == '__main__':
    with operate_file('a.txt', 'w') as f:
        print('写入一句话...')
        f.write('Hello Python.')

上面的operate_file()是一个被装饰器contextmanager装饰的生成器函数(函数体带有yield语句)。而yield之前的代码,就相当于__enter__里的内容,yield 之后的代码,就相当于__exit__ 里的内容。

基于生成器的上下文管理器的方法看起来更加直观和简单,但是这个方法需要你了解装饰器、生成器和yield语句的相关知识。

异常处理

在Python中,我们通常会使用try...except...语句来捕获异常。假如我们在主逻辑代码中使用了大量的异常捕捉语句,就会让代码看起来很不雅观,大大影响了代码的可读性。

因此,我们可以将异常处理逻辑放在上下文管理器中,在主逻辑代码中使用with语句,这样就可以让整体代码看起来更加简洁。

我们来看一下下面这个会产生异常的例子

class Calculator:
    def __enter__(self):
        print('计算中...')
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print(f'exc_type: {exc_type}')
        print(f'exc_val: {exc_val}')
        print(f'exc_tb: {exc_tb}')
        print('计算完成...')
        return True

    def divide(self):
        print('正在计算 1 / 0 ...')
        1 / 0


if __name__ == '__main__':
    with Calculator() as cal:
        cal.divide()

输出结果如下

计算中...
正在计算 1 / 0 ...
exc_type: <class 'ZeroDivisionError'>
exc_val: division by zero
exc_tb: <traceback object at 0x0000011A0987E848>
计算完成...

运行的程序没有报错,是因为上下文管理器帮我们捕获了异常。上下文管理器主要是在__exit__()方法里捕获了异常信息,它包含三个重要参数,如下:

  • exc_type:异常类型
  • exc_val:异常值
  • exc_tb:异常的错误栈信息

当主逻辑代码没有报异常时,这三个参数将都为None。

__exit__()方法里返回了 True(没有return就默认返回False,返回False会对外抛出异常),说明异常已经被捕获了,相当于告诉Python解释器,无须再往外抛出异常,至于在里面如何处理异常就由你自己决定了。

拓展

上面介绍了上下文管理器的基本概念和实现方法,不过基本上都是以操作文件为例子。在实际开发中,可以使用上下文管理器的地方也不少,接下来我们运用上面的知识点写一个操作数据库的上下文处理器。

import pymysql

MSCONFIG = {
    'host': 'localhost',
    'port': 3306,
    'user': 'root',
    'passwd': '123456',
    'db': 'myclass',
    'charset': 'utf8',
}


class MysqlOperation:

    def select(self, sql):
        """查询数据"""
        n = self.cur.execute(sql)  # 执行sql查询语句
        return self.cur.fetchmany(n)

    def execute(self, sql):
        """执行sql"""
        self.cur.execute(sql)
        self.conn.commit()  # 提交事务

    def __enter__(self):
        self.conn = pymysql.connect(**MSCONFIG)  # 连接
        self.cur = self.conn.cursor()  # 游标
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        """退出时关闭游标关闭连接"""
        if exc_type:
            print('遇到异常了,异常信息为:', exc_type, exc_val, exc_tb)
            self.conn.rollback()  # 回滚
        self.cur.close()
        self.conn.close()
        return True


if __name__ == '__main__':
    with MysqlOperation() as mo:
        sql = 'SELECT * FROM student'
        data = mo.select(sql)
        print(data)

结果如下

((1, 'Tony', 19, 'male'), (2, 'Lisa', 18, 'female'), (3, 'Jack', 20, 'male'))

如果执行的过程中出现异常,则会在上下文处理器中处理异常,如我们将sql改为SELECT * FROM student2(student2为不存在的表),得到的结果如下

遇到异常了,异常信息为: <class 'pymysql.err.ProgrammingError'> (1146, "Table 'myclass.student2' doesn't exist") <traceback object at 0x000001BCF2A85F88>

有了这个上下文处理器,我们以后就可以在主方法中使用with语句获取上下文管理器对象来操作数据库了。

总结

我认为使用上下文管理有如下几个好处:

  • 提升代码的可读性,让代码变得简洁优雅;
  • 提升代码的复用率,可以把常用的操作放到__enter__()__exit__()方法中;
  • 可以用更优雅的方式来处理异常。
posted @ 2021-09-18 17:41  蓝莓薄荷  阅读(368)  评论(0编辑  收藏  举报