Python---上下文管理器(contextor)

一 with语句就是简洁版的try/finally语句

在我们日常使用场景中,经常会操作一些资源,比如文件对象、数据库连接、Socket连接等,资源操作完了之后,不管操作的成功与否,最重要的事情就是关闭该资源,否则资源打开太多而没有关闭,程序会报错,以文件操作为例,通常我们会这样写:

f = open('file.txt', 'w')
try:
    f.write("Hello")
finally:
    f.close()

但既然close方法是必须的操作,那就没必要显式地调用,所以Python给我们提供了一种更优雅的方式,使用with语句:

with open('file.txt', 'w') as f:
    f.write("Hello")

在退出with语句下的代码块之后,f 对象会自动执行自己的close方法,实现资源的释放,简洁优雅。

事实上,上面一段代码就用到了上下文管理器的知识。

某种程度上,上下文管理器可以理解成try/finally的优化,使得代码更加易读,在通常情况下,我们读取文件的时候,如果不适用with语句,为了防止出错,可以采用try/finally的语句来进行读取,使得文件可以正常执行close()方法。

f = open('file.text', 'w'):
    try:
        f.write('hello')
    finally:
        f.close()
        

很明显,with语句比try/finally更易读,更友好。

 

二 上下文管理器原理

 

上下文管理器协议,是指要实现对象的 __enter__()__exit__() 方法。

上下文管理器也就是支持上下文管理器协议的对象,也就是实现了 __enter__()__exit__() 方法。

上下文管理器 是一个对象,它定义了在执行 with 语句时要建立的运行时上下文。 上下文管理器处理进入和退出所需运行时上下文以执行代码块。 通常使用 with 语句来使用,但是也可以通过直接调用它们的方法来使用。

简单来说,我们定义一个上下文管理器,需要在一个类里面一个实现__enter__(self)__exit__(self, exc_type, exc_value, traceback) 方法。

  • object.__enter__(self)

    进入与此对象相关的运行时上下文,并返回自身或者另一个与运行食上下文相关的对象。(with语句将会绑定这个方法的返回值到 as 子句中指定的目标)

  • object.__exit__(self, exc_type, exc_value, traceback)

    退出关联到此对象的运行时上下文。 各个参数描述了导致上下文退出的异常。 如果上下文是无异常地退出的,三个参数都将为None。如果提供了异常,并且希望方法屏蔽此异常(即避免其被传播),则应当返回真值。 否则的话,异常将在退出此方法时按正常流程处理。请注意__exit__()方法不应该重新引发被传入的异常,这是调用者的责任。如果 with_body 的退出由异常引发,并且__exit__()的返回值等于 False,那么这个异常将被重新引发一次;如果 __exit__() 的返回值等于 True,那么这个异常就被无视掉,继续执行后面的代码。

通常情况下,我们会使用with语句来使用上下文管理器:

with context_expr [as var]:
    with_body

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

大致对with语句的执行原理总结

  1. 执行 contextor 以获取上下文管理器
  2. 加载上下文管理器的 exit() 方法以备稍后调用
  3. 调用上下文管理器的 enter() 方法
  4. 如果有 as var 从句,则将 enter() 方法的返回值赋给 var
  5. 执行子代码块 with_body
  6. 调用上下文管理器的 exit() 方法,如果 with_body 的退出是由异常引发的,那么该异常的 type、value 和 traceback 会作为参数传给 exit(),否则传三个 None
  7. 如果 with_body 的退出由异常引发,并且 exit() 的返回值等于 False,那么这个异常将被重新引发一次;如果 exit() 的返回值等于 True,那么这个异常就被无视掉,继续执行后面的代码

了解了with语句和上下文管理协议,或许对上下文有了一个更清晰的认识。即代码或函数执行的时候,调用函数时候有一个环境,在不同的环境调用,有时候效果就不一样,这些不同的环境就是上下文。例如数据库连接之后创建了一个数据库交互的上下文,进入这个上下文,就能使用连接进行查询,执行完毕关闭连接退出交互环境。创建连接和释放连接都需要有一个共同的调用环境。不同的上下文,通常见于异步的代码中。

把文章开头的例子用上下文管理器实现一边:

class OpenFile(object):
    def __init__(self, filename):
        self.file = open(filename, 'w+')

    def __enter__(self):
        return self.file

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


def main():
    with OpenFile('text.txt') as f:
        f.write('ok')

        
if __name__ == "__main__":
    main()

总结:在上下文管理器中,生成类实例的时候,会自动调用__enter__()方法,而在结束的时候,会自动调用__exit__()方法。

所以,在定义上下文管理器的时候,我们只需实现好这两个方法就行了。

 

enterexit方法说明

1. enter方法说明

上下文管理器的enter方法是可以带返回值的,默认返回None,这个返回值通过with...as...中的 as 赋给它后面的那个变量,所以 with EXPR as VAR 就是将EXPR对象enter方法的返回值赋给 VAR

当然with...as...并非固定组合,单独使用with...也是可以的,上下文管理器的enter方法还是正常执行,只是这个返回值并没有赋给一个变量,with下面的代码块也不能使用这个返回值。

2. exit方法说明

上下文管理器的exit方法接收3个参数exc_type、exc_val、exc_tb,如果代码块BLOCK发生了异常e并退出,这3个参数分别为type(e)、str(e)、e.__traceback__,否则都为None。

同样exit方法也是可以带返回值的,这个返回值应该是一个布尔类型True或False,默认为None(即False)。如果为False,异常会被抛出,用户需要进行异常处理。如果为True,则表示忽略该异常。

 

 

三 上下文管理器的运用场景

上下文管理器的典型用法包括保存和恢复各种全局状态,锁定和解锁资源,关闭打开的文件等。

资源的创建和释放场景

上下文管理器的常用于一些资源的操作,需要在资源的获取与释放相关的操作,一个典型的例子就是数据库的连接,查询,关闭处理。先看如下一个例子:

class Database(object):

    def __init__(self):
        self.connected = False

    def connect(self):
        self.connected = True

    def close(self):
        self.connected = False

    def query(self):
        if self.connected:
            return 'query data'
        else:
            raise ValueError('DB not connected ')
            
def handle_query():
    db = Database()
    db.connect()
    print 'handle --- ', db.query()
    db.close()

def main():
    handle_query()

if __name__ == '__main__':
    main()

上述的代码很简单,针对Database这个数据库类,提供了connect queryclose 三种常见的db交互接口。客户端的代码中,需要查询数据库并处理查询结果。当然这个操作之前,需要连接数据库(db.connect())和操作之后关闭数据库连接( db.close())。上述的代码可以work,可是如果很多地方有类似handle_query的逻辑,连接和关闭这样的代码就得copy很多遍,显然不是一个优雅的设计。

对于这样的场景,下面使用装饰器进行改写如下:


class Database(object):
    ...
    
def dbconn(fn):
    def wrapper(*args, **kwargs):
        db = Database()
        db.connect()
        ret = fn(db, *args, **kwargs)
        db.close()
        return ret
    return wrapper

@dbconn
def handle_query(db=None):
    print 'handle --- ', db.query()
    
def main():
    ...

编写一个dbconn的装饰器,然后在针对handle_query进行装饰即可。使用装饰器,复用了很多数据库连接和释放的代码逻辑,看起来不错。装饰器解放了生产力。可是,每个装饰器都需要事先定义一下db的资源句柄,看起来略丑,不够优雅。

 

优雅的With as语句

Python提供了With语句语法,来构建对资源创建与释放的语法糖。给Database添加两个魔法方法:

class Database(object):

    ...
    
    def __enter__(self):
        self.connect()
        return self

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

然后修改handle_query函数如下:

def handle_query():
    with Database() as db:
        print 'handle ---', db.query()

在Database类实例的时候,使用with语句。一切正常work。比起装饰器的版本,虽然多写了一些字符,但是代码可读性变强了。

 

closing

返回一个上下文管理器,在完成代码块的时候会关闭参数

源码参考:

class closing(AbstractContextManager):

    def __init__(self, thing):
        self.thing = thing
    def __enter__(self):
        return self.thing
    def __exit__(self, *exc_info):
        self.thing.close()

常见用法,如写爬虫的时候,可以这样写:

from contextlib import closing
import requests

url = 'http://www.baidu.com'
with closing(requests.get(url)) as page:
    for line in page:
        print(page)

上下文管理器查询数据库

代码:

import pymysql


class Database(object):
    def __init__(self):
        self.db = pymysql.connect("localhost", "root", "root", "test")
        self.cursor = self.db.cursor()

    def query(self, sql):
        self.cursor.execute(sql)
        result = self.cursor.fetchone()
        return result

    def __enter__(self):   
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.cursor.close()
        self.db.close()



def main():
    sql = "SELECT password FROM USER WHERE username='{}' ORDER BY 1;".format('admin')
    with Database() as s:
        a = s.query(sql)
        print(a)
        

if __name__ == "__main__":
    main()

 

四 contextilb模块

比如我们需要在一段代码中使用到数据库的查询,可以通过上下文处理器来优化我们的代码结构,

contextilb模块

contextilb模块是python内置模块中的一个用于上下文的模块,可以让我们更优雅地使用上下文管理器。

@contextmanager

这是contextlib模块提供的一个装饰器,用于将一个函数声明上下文管理,无需创建一个类或者单独的__enter__()方法和__exit__()方法,就可以实现上下文管理。

需要注意的是,被装饰的函数被调用的时候必须返回一个生成器,而且这个生成器只生成一个值,如果有as的话,该值讲绑定到with语句as子句的目标中。

from contextlib import contextmanager


@contextmanager
def tag(name):
    print('<{}>'.format(name))
    yield
    print(''.format(name))


with tag('title'):
    print("This is a contextmanger test")

输出为:


This is a contextmanger test

可以看出,输出的流程:

  1. 先输出yield前的输出语句;
  2. 然后再是tag()函数的输出语句,
  3. 最后是yield后面的输出语句。

在生成器函数中的yield之前的语句在__enter__()方法中执行,

相当于

def __enter__(self):
    print('<{}>'.format(name))
    
def __exit__(self, exc_type, exc_val, exc_tb):
    print(''.format(name))

 

使用contextlib模块编写:

import contextlib
class Database(object):
    def __init__(self):
        self.db = pymysql.connect("localhost", "root", "root", "test")
        self.cursor = self.db.cursor()

    def query(self, sql):
        self.cursor.execute(sql)
        result = self.cursor.fetchone()
        return result


@contextlib.contextmanager
def database_query():
    q = Database()
    yield q


def main():
    sql = "SELECT password FROM USER WHERE username='{}' ORDER BY 1;".format('admin')
    with database_query() as s:
        a = s.query(sql)
        print(a)


if __name__ == "__main__":
    main()

 

通过实现上下文协议定义创建上下文管理器很方便,Python为了更优雅,还专门提供了一个模块用于实现更函数式的上下文管理器用法。

import contextlib

@contextlib.contextmanager
def database():
    db = Database()
    try:
        if not db.connected:
            db.connect()
        yield db
    except Exception as e:
        db.close()

def handle_query():
    with database() as db:
        print 'handle ---', db.query()

使用contextlib 定义一个上下文管理器函数,通过with语句,database调用生成一个上下文管理器,然后调用函数隐式的__enter__方法,并将结果通yield返回。最后退出上下文环境的时候,在excepit代码块中执行了__exit__方法。当然我们可以手动模拟上述代码的执行的细节。

In [1]: context = database()    # 创建上下文管理器

In [2]: context


In [3]: db = context.__enter__() # 进入with语句

In [4]: db                          # as语句,返回 Database实例
Out[4]: <__main__.Database at 0x107188a10>

In [5]: db.query()       
Out[5]: 'query data'

In [6]: db.connected
Out[6]: True

In [7]: db.__exit__(None, None, None)    # 退出with语句

In [8]: db
Out[8]: <__main__.Database at 0x107188a10>

In [9]: db.connected
Out[9]: False

上下文管理器的用法

既然了解了上下文协议和管理器,当然是运用到实践啦。通常需要切换上下文环境,往往是在多线程/进程这种编程模型。当然,单线程异步或者协程的当时,也容易出现函数的上下文环境经常变动。

异步式的代码经常在定义和运行时存在不同的上下文环境。此时就需要针对异步代码做上下文包裹的hack。看下面一个例子:


import tornado.ioloop

ioloop = tornado.ioloop.IOLoop.instance()


def callback():
    print 'run callback'
    raise ValueError('except in callback')

def async_task():
    print 'run async task'
    ioloop.add_callback(callback=callback)

def main():

    try:
        async_task()
    except Exception as e:
        print 'exception {}'.format(e)
    print 'end'

main()
ioloop.start()

运行上述代码得到如下结果

run async task
end
run callback
ERROR:root:Exception in callback 
Traceback (most recent call last):
  ...
    raise ValueError('except in callback')
ValueError: except in callback


主函数中main中,定义了异步任务函数async_task的调用。async_task中异常,在except中很容易catch,可是callback中出现的异常,则无法捕捉。原因就是定义的时候上下文为当前的线程执行环境,而使用了tornado的ioloop.add_callback方法,注册了一个异步的调用。当callback异步执行的时候,他的上下文已经和async_task的上下文不一样了。因此在main的上下文,无法catch异步中callback的异常。

下面使用上下文管理器包装如下:

class Contextor(object):
    def __enter__(self):
        pass

    def __exit__(self, exc_type, exc_val, exc_tb):
        if all([exc_type, exc_val, exc_tb]):
            print 'handler except'
            print 'exception {}'.format(exc_val)
        return True

def main():
    with tornado.stack_context.StackContext(Contextor):
        async_task()
        
运行main之后的结果如下:

run async task
handler except
run callback
handler except
exception except in callback

可见,callback的函数的异常,在上下文管理器Contextor中被处理了,也就是说callback调用的时候,把之前main的上下文保存并传递给了callback。当然,上述的代码也可以改写如下:


@contextlib.contextmanager
def contextor():
    try:
        yield
    except Exception as e:
        print 'handler except'
        print 'exception {}'.format(e)
    finally:    
        print 'release'

def main():
    with tornado.stack_context.StackContext(contextor):
        async_task()
        

效果类似。当然,也许有人会对StackContext这个tornado的模块感到迷惑。其实他恰恰应用上下文管理器的魔法的典范。查看StackContext的源码,实现非常精秒,非常佩服tornado作者的编码设计能力.

 

 

参考 https://www.jianshu.com/p/b824b9e2ab8c

你可能感兴趣的:(Python)