专栏名称: Python开发者
人生苦短,我用 Python。伯乐在线旗下账号「Python开发者」分享 Python 相关的技术文章、工具资源、精选课程、热点资讯等。
目录
相关文章推荐
Python爱好者社区  ·  DeepSeek 被放弃了,阿里牛逼! ·  2 天前  
Python爱好者社区  ·  刚刚,DeepSeek开源MoE训练、推理E ... ·  2 天前  
Python开发者  ·  清北 DeepSeek ... ·  2 天前  
Python开发者  ·  OpenAI ... ·  3 天前  
Python中文社区  ·  “快得飞起,不卡不掉线!”量化交易必备的De ... ·  3 天前  
51好读  ›  专栏  ›  Python开发者

python 黑魔法 ---上下文管理器(contextor)

Python开发者  · 公众号  · Python  · 2017-02-10 21:29

正文

(点击 上方蓝字 ,快速关注我们)


来源:人世间

www.jianshu.com/p/d53449f9e7e0

如有好文章投稿,请点击 → 这里了解详情


所谓上下文


计算机上下文(Context)对于我而言,一直是一个很抽象的名词。就像形而上一样,经常听见有人说,但是无法和现实认知世界相结合。


最直观的上下文,莫过于小学的语文课,经常会问联系上下文,推测...,回答...,表明作者...。文章里的上下文比较好懂,无非就是前与后。


直到了解了计算机的执行状态,程式的运行,才稍微对计算机的上下文(context)有了一定的认识,多半还是只可意会,不可言传。本文所讨论的上下文,简而言之,就是程式所执行的环境状态,或者说程式运行的情景。


关于上下文的定义,我就不在多言,具体通过程式来理解。既然提及上下文,就不可避免的涉及Python中关于上下文的魔法,即上下文管理器(contextor)。


资源的创建和释放场景


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


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


对于这样的场景,在python黑魔法—装饰器中有讨论如何优雅的处理。下面使用装饰器进行改写如下:


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。比起装饰器的版本,虽然多写了一些字符,但是代码可读性变强了。


上下文管理协议


前面初略的提及了上下文,那什么又是上下文管理器呢?与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语句代码块不会显示的抛出异常,终止程序,如果返回None或者False,异常会被主动raise,并终止程序。


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


上下文管理器工具


通过实现上下文协议定义创建上下文管理器很方便,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返回。最后退出上下文环境的时候,在except代码块中执行了__exit__方法。当然我们可以手动模拟上述代码的执行的细节。


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

In [ 2 ] : context

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

In [ 4 ] : db # as语句,返回 Database实例

Out [ 4 ] :

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 ] :

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 {}' .







请到「今天看啥」查看全文