python装饰器

本文先介绍with表达式,然后再试图用with以及装饰器等知识实现自己的ContextManager 

with可以干什么?我的理解是简化try except finally的工作,比如打开文件操作符,读文件,捕捉异常,最后关闭。这个例子是with最最常用的方法了,满大街都可以找到这个例子。 
除文件open操作之外,其实其它很多操作也可以掐头去尾,留下中间关键操作就行。 
那么该如何实现呢?按照python文档解释,只要实现__enter__和__exit__两个函数就可以。 
很简单: 

Python代码  收藏代码

  1. class ConMgr(object):  

  2.     def __init__(self):  

  3.         print("__init__ called")  

  4.       

  5.       

  6.     def __call__(self, *args):  

  7.         print("__call__ called")  

  8.         #: 可以把传进来的参数保存着,在with开始时运行  

  9.         self.args = args  

  10.         return self  

  11.           

  12.       

  13.     def __enter__(self):  

  14.         # 打印之前传进来的参数  

  15.         print("__enter__ called"self.args)  

  16.         return 'abcd'  

  17.   

  18.           

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

  20.         print("__exit__ called: exc_type = %s exc_val = %s exc_tb = %s "\  

  21.               % (exc_type, exc_val, exc_tb))  

  22.         return "exited"  

  23.   

  24.   

  25. def test1():  

  26.     c = ConMgr()  

  27.     with c('pppp') as tmp:  

  28.         print(tmp)  

  29.         print("haha")  

  30.         assert 1>2  

  31.         print(3)  

  32.   

  33. test1()  


输出结果是: 

Python代码  收藏代码

  1. __init__ called  

  2. __call__ called  

  3. ('__enter__ called', ('pppp',))  

  4. abcd  

  5. haha  

  6. __exit__ called: exc_type = <type 'exceptions.AssertionError'> exc_val =  exc_tb = <traceback object at 0x7f911003b128>   


首先请注意,这里的c()是类的一个实例,就是一个普通类,而不是generator。当c('pppp')执行时,调用了__call__函数,__call__函数赶紧把传入的参数保存了下来,等进了with块之后,调用__enter__之时再把参数放出来。 
另外__call__函数一定要返回self,因为with块运行完了之后,将会调用self.__exit__()如果不返回self将找不到__exit__函数。 
然后就是as语句的tmp值实际上是__enter__的返回值,返回什么都可以,无所谓的,哪怕传个闭包。这里的好主意是把之前的args可以传给tmp。 
最后请仔细看,当assert 1>2发生错误之后,with块没有执行完就调用__exit__函数了。通过这个函数的参数,我们来实现异常处理。 

接下来介绍下python的contextlib这个模块。 
可能有朋友不知到,这个模块没有主轴功能,主要是围绕with语句,提供了一些方便的util函数操作。 
这个模块里面有一个contextmanager的装饰器,它可以省掉我们之前那么麻烦创建一个class然后补上__enter__和__exit__的过程,它利用工厂模式生成一个generator,然后就可以方便的使用with语句了。 
关于官方contextlib模块里面的功能,我想自己能不能做一个山寨版出来

contextlib.contextmanager的用法是怎样的?我摘抄一下模块源代码 

引用

    Typical usage: 

        @contextmanager 
        def some_generator(<arguments>): 
            <setup> 
            try: 
                yield <value> 
            finally: 
                <cleanup> 

    This makes this: 

        with some_generator(<arguments>) as <variable>: 
            <body> 

    equivalent to this: 

        <setup> 
        try: 
            <variable> = <value> 
            <body> 
        finally: 
            <cleanup>


大致就是可以把程序中的主体,<body>从中抽取出来,放到with块之中。而之后处理的<finally>都可以放到some_generator函数里面。 
some_generator函数的编写很好办。只是contextmanager这个装饰器应该作为一个函数好呢还是一个类好呢?这是个很有意思的问题,我也想了好一会,等下一起讨论下。先给出我的解法: 

Python代码  收藏代码

  1. class MyGeneratorContextManager(object):  

  2.     def __init__(self, gen):  

  3.         print("__init__ called")  

  4.         self.gen = gen  

  5.       

  6.       

  7.     def __enter__(self):  

  8.         print("__enter__ called")  

  9.         return self.gen.next()  

  10.       

  11.       

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

  13.         print("__exit__called exc_type = %s, exc_val = %s, exc_tb = %s"\  

  14.               % (exc_type, exc_val, exc_tb))  

  15.         # 这里没有做异常处理,需要处理StopIteration异常  

  16.         # 不是用return也可以  

  17.         # 下面这句话将输出yield [1, 2, 3]后面的的打印语句end foo  

  18.         return self.gen.next()  

  19.           

  20.       

  21. def MyContextManager(func):  

  22.     def tmpf(*args):  

  23.         print("func info:", func)  

  24.         return MyGeneratorContextManager(func(*args))  

  25.     return tmpf  

  26.  

  27.  

  28. @MyContextManager  

  29. def foo(val):  

  30.     # 尝试用老方法捕捉错误  

  31.     try:  

  32.         print("start foo", val)  

  33.         yield [123]  

  34.         # 下面一行需要调用self.gen.next()才能输出  

  35.         print("end foo")  

  36.     except (Exception, AssertionError):  

  37.         # 但是实际上并没有捕捉到yield中的错误  

  38.         # except的功能完全被__exit__取代  

  39.         print("EXCEPTION ENCOUNTERED!")  

  40.     finally:  

  41.         print("FINALLY")  

  42.   

  43.   

  44. print("foo is ", foo)  

  45. print("foo() is ", foo("bbbb"))  

  46. print("\nWITH INFO BELOW:")  

  47. with foo("aaaa") as tmp:  

  48.     print("START WITH")  

  49.     #: tmp实际上就是yield穿过来的值  

  50.     print(tmp)  

  51.     for i in tmp:  

  52.         print(i)  

  53.     assert 1>2  

  54.     # 出错之后直接从with中跳出去,下面不可能被执行  

  55.     print("END WITH")  


输出结果是: 

Python代码  收藏代码

  1. # 首先可以看到foo的值是闭包中的tmpf函数  

  2. ('foo is ', <function tmpf at 0x7fb78b15f140>)  

  3. ('func info:', <function foo at 0x7fb78b15f0c8>)  

  4. __init__ called  

  5. ('foo() is ', <__main__.MyGeneratorContextManager object at 0x7fb78b1591d0>)  

  6.   

  7. WITH INFO BELOW:  

  8. # 请看,两次调用foo(),发现他们最终都是同一个foo函数  

  9. ('func info:', <function foo at 0x7fb78b15f0c8>)  

  10. # 但是奇怪的是,函数被初始化了两次?这是因为这是个工厂模式,每次调用的函数虽然一样,但是会生成不同的类  

  11. __init__ called  

  12. __enter__ called  

  13. ('start foo''aaaa')  

  14. START WITH  

  15. [123]  

  16. 1  

  17. 2  

  18. 3  

  19. # assert触发的错误没有被except捕捉到!被__exit__函数捕捉到了  

  20. __exit__called exc_type = <type 'exceptions.AssertionError'>, exc_val = , exc_tb = <traceback object at 0x7fb78b15c2d8>  

  21. end foo  

  22. FINALLY  

  23. # 为什么跳出StopIteration异常?这是因为gen.next()已经走到头了,我们没有处理这异常  

  24. Traceback (most recent call last):  

  25.   File "/home/leonardo/Aptana Studio 3 Workspace/PythonStudy/src/thinking/mycontext_manager.py", line 68in <module>  

  26.     print("END WITH")  

  27.   File "/home/leonardo/Aptana Studio 3 Workspace/PythonStudy/src/thinking/mycontext_manager.py", line 31in __exit__  

  28.     return self.gen.next()  

  29. StopIteration  


首先创建了一个工厂模式的MyContextManager,它实际上又是个闭包函数,也可以作为装饰器方便以后使用。 

其次我定义了一个类MyGeneratorContextManager,这个函数在初始化的时候,就接收一个generator。请注意,接收的是generator而不是function。 
请看函数 

Python代码  收藏代码

  1. def tmpf(*args):  

  2.         print("func info:", func)  

  3.         return MyGeneratorContextManager(func(*args))  


func在这里虽然是一个函数,但是func(*args)是一个generator,忘记传参数就糟了 

在__enter__中最紧要的是要 

Python代码  收藏代码

  1. return self.gen.next()  


因为我们在with中面对的是一个generator,如果不对其进行next(),这个函数是不会动的。 

进入到with块之后,foo函数里yield的值会直接传给tmp,这个值无关紧要。with块中所有语句就好像全部被填到yield那个地方去了一样。 

程序于是执行with块中的语句,当其中出现异常的时候,我们外围的except语句块应该迅速捕捉到这一点,并输出才对。实际上不是,实际上当foo函数出现异常的时候,__exit__函数是第一时间捕捉到这个异常的。通过它的打印信息我们可以看出。 

当处理完这个异常之后,我们调用了一下self.gen.next(),这个语句保证field语句后面的语句会被执行。最后执行了finally中的内容。你看except语句块被完全架空了。 

好,我们回过头来再看,我们到底实现了什么东西?我们想执行的是 

引用

<setup> 
        try: 
            <variable> = <value> 
            <body> 
        finally: 
            <cleanup>


现在我们把body单独抽出来了,放到with当中,把variable value替换成了yield value。这样做到掐头去尾,把前后不变的东西拿了出来,把内部的东西抽了出来。 
咦?这不就是个装饰器么?错。装饰器是保持真身不懂,把前后改了,这是两个不同的方面。 


最后我们回到我一开始提出的问题,为什么要设计一个mycontextmanager函数?可不可以不用工厂模式,直接用MyGeneratorContextManager函数一步到位? 
这有两个前提条件要注意。第一,如果用class作为装饰器,这个类必须是可调用的(callable),如果将来我们要使用它,只可能它的调用__call__函数,因为它又要实现能在call之后调用__exit__,所以它只能返回self, type(self)之类的东西。第二,因为foo()是用在with语句中的,所以它必须是一个generator,也就是说foo.__call__()的返回结果是一个generator。那么可以找到有这么一个返回值,它既是self, type(self)这样类相关的类型,而且还是一个包含yield的generator么?:-) 


要是对yield还不熟悉的朋友可能现在还不是很清楚,尤其是generator函数和with块中代码的关系,很可能还有一些把这代码再折腾折腾的想法。


你可能感兴趣的:(python,with,contextmanager)