Hi!请登陆

一文弄懂Python上下文管理器和with用法

2020-3-23 82 3/23
导读:pythoners都知道有个关键字叫”with”,它可以实现使用某些”临时”声明的对象,而之后”什么也不用管”,这个用法在python中叫上下文管理器。本文带你快速入门上下文管理器和with用法。

一文弄懂Python上下文管理器和with用法

01 初识

上下文管理器,英文context managers,在python官方文档中这样描述:

上下文管理器是一个对象,它定义了在执行 with 语句时要建立的运行时上下文。 上下文管理器处理进入和退出所需运行时上下文以执行代码块。 通常使用 with 语句(在 with 语句中描述),但是也可以通过直接调用它们的方法来使用。
那么可能还会有进一步的疑问:”上下文”又是什么呢?

一文弄懂Python上下文管理器和with用法

知乎某大佬关于”上下文”的解释

如果还是难懂,那就举个例子。在python中,写入文件的基本操作可能是这样的:
f = open('a.txt', 'w')
f.write('22')
f.close()
如果考虑在文件操作期间可能会触发异常、造成文件不能关闭,那么比较安全的改进写法是:
f = open('a.txt', 'w')
try:
    f.write('22')
except:
    print('File eroor!')
finally:
    f.close()
当使用with包装上下文管理器后,那么只需要这样:
with open('b.txt', 'w') as f:
    f.write('22')

可能会有这样疑问,不就是节省了一行文件关闭的代码而已嘛,也没看出有多高效啊!

这里先抛出结论:使用with管理上下文不仅可以在执行with语句体后自动执行退出操作(即__exit__方法定义语句),更重要的是能够在发生异常时,确保始终能执行退出操作、释放相应的资源。

02 详解

在PEP343(该PEP文档由python之父龟叔发起,最早在python2.5版本引入。如想了解更多关于PEP知识,可阅读PEP入门指南),对上下文管理器给出如下定义:

上下文管理器是指提供了一对专门方法__enter __()和__exit __(),这些方法在with语句的主体进入和退出时被调用。在Python语言中添加了一个“ with”新语句,从而可以排除try / finally语句的标准用法。

也就是说,如果某个类定义了__enter__、__exit__方法,允许在语句体执行前进入该上下文、执行后退出该上下文,那么这样的类就可以称作是上下文管理器对象。而且该用法可用作对try/finally的替代,以处理可能存在的异常。

前面举了文件操作的with用法,说明文件操作的对象是一个上下文管理器对象,那么我们先来看下文件操作类来怎样定义的:
##_pyio.py
### Context manager ###
def __enter__(self):  # That's a forward reference
    """Context management protocol.  Returns self (an instance of IOBase)."""
    self._checkClosed()
    return self

def __exit__(self, *args):
    """Context management protocol.  Calls close()"""
    self.close()
注:open()函数返回一个FileIO类对象,FileIO类继承自RawIOBase类,RawIOBase又继承自IOBase类,而IOBase类定义了__enter__和__exit__方法,因而是一个上下文管理器对象。
同时,在IOBase类说明文档中,也给出了这样介绍:
"""IOBase also supports the :keyword:`with` statement. In this example,
fp is closed after the suite of the with statement is complete:

with open('spam.txt', 'r') as fp:
    fp.write('Spam and eggs!')"""
再举个例子,在python并发之concurrent快速入门一文中,对比多线程和多进程的并发操作时,也使用了with包装上下文管理器的用法:
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
def multi_thread():
    with ThreadPoolExecutor() as executor:
        return list(executor.map(is_prime, PRIMES))

def multi_process():
    with ProcessPoolExecutor() as executor:
        return list(executor.map(is_prime, PRIMES))
那么有理由相信,concurrent.futures类肯定也定义了__enter__和__exit__方法,具体如下:
##_base.py
def __enter__(self):
    return self

def __exit__(self, exc_type, exc_val, exc_tb):
    self.shutdown(wait=True)
    return False
注:ThreadPoolExecutor和ProcessPoolExecutor两个类均继承自Executor类,而Excutor类是一个上下文管理器对象。需补充说明的是,正如上述示例代码给出的那样,__exit__()方法默认接收3个参数:exc_type、exc_val、exc_tb,如果with语句体发生异常,则3个参数分别赋值为type(error)、str(error)、error.__traceback__,否则均为None。

至此,我们可以得出这样的结论:一个上下文管理器对象是指定义了__enter__和__exit__方法的类对象;反之,定义了__enter__和__exit__方法的类可以应用with包装实现上下文管理器用法。

应用with包装上下文管理器时:执行with语句生成一个实例对象时,自动调用__enter__方法创建一个适用于上下文环境的类对象,完毕后无论是否触发异常都会调用__exit__方法执行退出操作。

03 实战
实际上,上下文管理器常适用于带有资源管理的操作,如前面例子中给出的文件操作和并发操作。这里,我们举一个操作数据库的例子,实现一个简单的打开数据库类。
import pymysql
class OpenMySQL(object):
    def __init__(self, db):
        self.conn = pymysql.connect(host="localhost", user="root", password="123456", db=db, charset='utf8')

    def __enter__(self):
        return self.conn.cursor()

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.conn.commit()
        self.conn.close()

if __name__ == '__main__':
    with OpenMySQL('mytest') as sql:
        sql_insert = 'insert into tbtest values (2);'
        sql.execute(sql_insert)

代码功能执行正常,且相比于基本的数据库connect()–commit()–close()操作流程来说,要优雅许多。

注:写完例子后查阅源码发现pymysql.connect方法返回对象本身就是一个上下文管理器对象……。但仅仅用于举例的话,这也足以说明with包装上下文管理器的用法了。

04 总结

本文对python中上下文管理器和with用法进行了简单介绍,包括:

  • 上下文管理器是一个实现了__enter__、__exit__魔法方法的类对象

  • 定义了__enter__、__exit__方法的类对象即可用作上下文管理器

  • 上下文管理器通常使用with语句实现
  • with语句可实现简洁版的try/finally替代用法

Tag:

相关推荐