zoukankan      html  css  js  c++  java
  • python 魔术方法上下文管理(__enter__,__exit__)

    上下文管理

    文件IO操作可以对文件对象使用上下文管理,使用with……as语法。

    with open("test") as f:
        pass

    仿照上例写一个自己的类,实现上下文管理。

    class Point:
        pass
    
    with Point() as p:
        pass
    
    结果为:
    ---------------------------------------------------------------------------
    AttributeError                            Traceback (most recent call last)
    <ipython-input-1-77f0ac1b02c7> in <module>
          2     pass
          3 
    ----> 4 with Point() as p:
          5     pass
    
    AttributeError: __enter__

    提示属性错误,没有__exit__,看了需要这个属性。

    上下文管理对象

    当一个对象同时实现了__enter__()和__exit__()方法,它就属于上下文管理对象。上下文管理器的主要原理是你的代码会放到 with 语句块中执行。 当出现 with 语句的时候,对象的 __enter__() 方法被触发, 它返回的值(如果有的话)会被赋值给 as 声明的变量。然后,with 语句块里面的代码开始执行。 最后,__exit__() 方法被触发进行清理工作。

    __enter__():进入于此相关的上下文。如果存在该方法,with语法会把该方法的返回值作为绑定到as子句中指定的变量上。

    __exit__:退出与此对象相关的上下文。

    class Point:
        def __init__(self):
            print("init")
            
        def __enter__(self):#进去的时候做的事
            print("enter")
            
        def __exit__(self,exc_type,exc_val,exc_tb):#离开的时候做的事情
            print("exit")
            
    with Point() as f:
        print("do sth.")
    
    结果为:
    init
    enter
    do sth.
    exit

    实例化对象的时候,并不会调用enter,而是进入with语句块调用__enter__方法,然后执行语句块,最后离开with语句块的时候,调用__exit__方法。

    with可以开启一个上下文运行环境,在执行前做一些准备工作,执行后做一些收尾工作。

    上下文管理的安全性

    看看异常对上下文管理的影响。

    class Point:
        def __init__(self):
            print("init")
            
        def __enter__(self):
            print("enter")
            
        def __exit__(self,exc_type,exc_val,exc_tb):
            print("exit")
            
    with Point() as f:
        raise Exception("error")
        print("do sth.")
    
    结果为:
    init
    enter
    exit
    ---------------------------------------------------------------------------
    Exception                                 Traceback (most recent call last)
    <ipython-input-4-28a514a324bc> in <module>
         10 
         11 with Point() as f:
    ---> 12     raise Exception("er")
         13     print("do sth.")
    
    Exception: er

    可以看到在enter和exit照样执行,上下文管理是安全的。

    极端的例子

    调用sys.exit(),它会退出当前解释器。

    打开Python解释器,在里面敲入sys.exit(),窗口直接关闭了,也就是说碰到这一句,Python运行环境直接退出了。

    import sys
    
    class Point:
        def __init__(self):
            print("init")
            
        def __enter__(self):
            print("enter")
            
        def __exit__(self,exc_type,exc_val,exc_tb):
            print("exit")
            
    with Point() as f:
        sys.exit(-100)
        print("do sth.")
    
    print("outer")
    
    结果为:
    init
    enter
    exit
    An exception has occurred, use %tb to see the full traceback.
    
    SystemExit: -100
    
    
    d:Anaconda3libsite-packagesIPythoncoreinteractiveshell.py:3304: UserWarning: To exit: use 'exit', 'quit', or Ctrl-D.
      warn("To exit: use 'exit', 'quit', or Ctrl-D.", stacklevel=1)

    从执行结果来看,依然执行了__exit__函数,哪怕是退出Python运行环境,说明上下文管理很安全。

    class Point:
        def __init__(self):
            print("init")
            
        def __enter__(self):
            print("enter"+self.__class__.__name__)
            return self
        
        def __exit__(self,exc_type,exc_val,exc_tb):
            print("exit"+self.__class__.__name__)
            print(exc_type)
            print(exc_val)
            print(exc_tb)
            return 1
            
    p = Point()
    with p as f:
        raise Exception("error123")
        
        print(f==p)
        print(f is p)
        print(f)
        print(p)
        
    
    结果为:
    init
    enterPoint
    exitPoint
    <class 'Exception'>
    error123
    <traceback object at 0x03AE2FD0>
    class Point:
        def __init__(self):
            print("init")
            
        def __enter__(self):
            print("enter"+self.__class__.__name__)
            return self
        
        def __exit__(self,exc_type,exc_val,exc_tb):
            print("exit"+self.__class__.__name__)
            print(exc_type)
            print(exc_val)
            print(exc_tb)
            return 0
            
    p = Point()
    with p as f:
        raise Exception("error123")
        
        print(f==p)
        print(f is p)
        print(f)
        print(p)
        
    
    结果为:
    init
    enterPoint
    exitPoint
    <class 'Exception'>
    error123
    <traceback object at 0x03D30F58>
    ---------------------------------------------------------------------------
    Exception                                 Traceback (most recent call last)
    <ipython-input-6-628d15869af5> in <module>
         16 p = Point()
         17 with p as f:
    ---> 18     raise Exception("error123")
         19 
         20     print(f==p)
    
    Exception: error123

    由上面的例子可以看到,返回值很重要,一个返回值可以压制异常,是外面管还是里面管,返回等效true的话就是里面管,返回false的话就是外面管。所以这两个函数的返回值都很重要。

    with语句

    class Point:
        def __init__(self):
            print("init")
            
        def __enter__(self):
            print("enter")
            
        def __exit__(self,exc_type,exc_val,exc_tb):print("exit")
            
    
    p = Point()
    with p as f:
        print(p == f)#为什么不相等
        print("do sth.")
    
    结果为:
    init
    enter
    False
    do sth.
    exit
     

    问题在于__enter__方法,它将自己的返回值赋给f,修改上例。

    class Point:
        def __init__(self):
            print("init")
            
        def __enter__(self):
            print("enter")
            return self
            
        def __exit__(self,exc_type,exc_val,exc_tb):
            print("exit")
    
    p = Point()
    with p as f:
        print(p == f)#为什么不相等
        print("do sth.")
    
    结果为:
    init
    enter
    True
    do sth.
    exit

    __enter__方法返回值就是上下文中使用的对象,with语法会把它的返回值赋给as子句的变量。

    __enter__方法和__exit__方法的参数

    __enter__方法没有其他参数。

    __exit__方法有三个参数。

    __exit__(self,exc_type,exc_value,traceback)

    这三个参数都是与异常相关,如果该上下文退出时没有异常,这3个参数都为None。如果有异常,参数意义如下:

    exc_type:异常类型

    exc_vaule:异常的值

    traceback:异常的追踪信息。

    __exit__方法返回一个等效true的值,则压制异常,否则,继续抛出异常。

    class Point:
        def __init__(self):
            print("init")
            
        def __enter__(self):
            print("enter")
            
        def __exit__(self,exc_type,exc_val,exc_tb):
            print(exc_type)
            print(exc_val)
            print(exc_tb)
            print("exit")
            return "abc"
    
    p = Point()
    with p as f:
        raise Exception("new error")
        print("do sth.")
    
    print("outer")
    
    结果为:
    init
    enter
    <class 'Exception'>
    new error
    <traceback object at 0x0000000005A7A088>
    exit
    outer

    练习

    为加法函数计时

    方法1、使用装饰器显示该函数的执行时长

    方法2、商用上下文管理方法来显示该函数的执行时长。

    import time
    
    def add(x,y):
        time.sleep(2)
        return x+y

    装饰器实现

    import time
    import datetime
    from functools import wraps
    
    def timeit(fn):
        @wraps(fn)
        def wrapper(*args,**kwargs):
            start = datetime.datetime.now()
            ret = fn(*args,**kwargs)
            delta = (datetime.datetime.now()-start).total_seconds()
            print("{} took {}s".format(fn.__name__,delta))
            return ret
        return wrapper
    
    @timeit
    def add(x,y):
        time.sleep(2)
        return x+y
    
    print(add(4,5))
    
    结果为:
    add took 2.0s
    9

    上下文管理

    import time
    import datetime
    from functools import wraps
    
    def add(x,y):
        time.sleep(2)
        return x+y
    
    class Timeit():
        def __init__(self,fn):
            self.fn = fn
            
        def __enter__(self):
            self.start =datetime.datetime.now()
            
        def __exit__(self,exc_type,exc_val,exc_tb):
            delta = (datetime.datetime.now()-self.start).total_seconds()
            print("{} took {}s".format(self.fn.__name__,delta))
            
    with Timeit(add) as fn:
        print(add(4,7))
    
    结果为:
    11
    add took 2.001s

    另一种实现,使用可调用对象实现。

    import time
    import datetime
    from functools import wraps
    
    def add(x,y):
        time.sleep(2)
        return x+y
    
    class Timeit():
        def __init__(self,fn):
            self.fn = fn
            
        def __enter__(self):
            self.start =datetime.datetime.now()
            return self
            
        def __exit__(self,exc_type,exc_val,exc_tb):
            delta = (datetime.datetime.now()-self.start).total_seconds()
            print("{} took {}s".format(self.fn.__name__,delta))
            
        def __call__(self,x,y):
            print(x,y)
            return self.fn(x,y)
        
    with Timeit(add) as timeitobj:
        print(timeitobj(4,5))

    根据上面的代码,能不能把类当做装饰器用?

    import time
    import datetime
    from functools import wraps
    
    class Timeit:
        """ this is a class"""
        def __init__(self,fn):
            self.fn = fn
            #把函数对象的文档字符串赋值给类
            self.__doc__ = fn.__doc__
            #update_wrapper(self,fn)
            
        def __enter__(self):
            self.start = datetime.datetime.now()
            return self
        
        def __exit__(self,exc_type,exc_val,exc_tb):
            delta = (datetime.datetime.now()-self.start).total_seconds()
            print("{} took {}s".format(self.fn.__name__,delta))
            
        def __call__(self,*args,**kwargs):
            self.start = datetime.datetime.now()
            ret = self.fn(*args,**kwargs)
            delta = (datetime.datetime.now()-self.start).total_seconds()
            print("{} took {}s".format(self.fn.__name__,delta))
            return ret
        
    @Timeit
    def add(x,y):
        """ this is add function"""
        time.sleep(2)
        return x+y
    
    print(add(10,5))
    print(add.__doc__)
    
    print(Timeit(add).__doc__)
    
    结果为:
    add took 2.018236s
    15
     this is add function
     this is add function

    思考,如何解决文档字符串问题?

    方法一:直接修改__doc__

    class Timeit:
        def __init__(self,fn = None):
            self.fn = fn
            #把函数对象的文档字符串赋给类
            self.__doc__ = fn.__doc__

    方法二:使用functools.wraps函数

    import time
    import datetime
    from functools import wraps
    
    class Timeit:
        """ this is a class"""
        def __init__(self,fn):
            self.fn = fn
            #把函数对象的文档字符串赋值给类
            self.__doc__ = fn.__doc__
            #update_wrapper(self,fn)
            
        def __enter__(self):
            self.start = datetime.datetime.now()
            return self
        
        def __exit__(self,exc_type,exc_val,exc_tb):
            delta = (datetime.datetime.now()-self.start).total_seconds()
            print("{} took {}s".format(self.fn.__name__,delta))
            
        def __call__(self,*args,**kwargs):
            self.start = datetime.datetime.now()
            ret = self.fn(*args,**kwargs)
            delta = (datetime.datetime.now()-self.start).total_seconds()
            print("{} took {}s".format(self.fn.__name__,delta))
            return ret
        
    @Timeit
    def add(x,y):
        """ this is add function"""
        time.sleep(2)
        return x+y
    
    print(add(10,5))
    print(add.__doc__)
    
    print(Timeit(add).__doc__)
            

    上面的类即可以用在上下文管理,又可以用作装饰器,初始化init函数不写return不代表返回是none,它返回的是实例。

    上下文应用场景

    1.增强功能

    在代码执行的前后增加代码,以增强其功能,类似装饰器的功能。

    2.资源管理

    打开了资源需要关闭,例如文件对象,网络连接,数据库连接

    3.权限验证

    在执行代码之前,做权限的验证,在__enter__中处理。

    contexlib.contextanmger

    contexlib.contextanmger是一个装饰器实现上下文管理, 装饰一个函数,而不用像类一样实现__enter__和__exit__方法。

    对下面的函数有要求,必须有yield,也就是这个函数必须返回一个生成器,且只有yield一个值,也就是这个装饰器接收一个生成器对象作为参数。

    import contextlib
    
    @contextlib.contextmanager
    def foo():
        print("enter")#相当于__enter__()
        yield #yield 5,yield的值只能有一个,作为__enter__方法的返回值
        print("exit") #相当于__exit__()
        
    with foo() as f:
        #raise Exception()
        print(f)
    
    结果为:
    enter
    None
    exit
     
    import contextlib
    
    @contexlib.contextanmger
    def foo():
        print("enter")#相当于__enter__()
        yield #yield 5,yield的值只能有一个,作为__enter__方法的返回值
        print("exit") #相当于__exit__()
        
    with foo() as f:
        #raise Exception()
        print(f)
    
    结果为:
    enter
    None
    exit
     

    f接收yield语句的返回值

    上面的程序看似不错,但是,增加一个异常试一试,发现不能保证exit的执行,怎么办?

    import contextlib
    
    @contextlib.contextmanager
    def foo():
        print("enter")#相当于__enter__()
        try:
            yield #yield 5,yield的值只能有一个,作为__enter__方法的返回值
        finally:
            print("exit") 
        
    with foo() as f:
        raise Exception()
        print(f)
    
    结果为:
    enter
    exit
    ---------------------------------------------------------------------------
    Exception                                 Traceback (most recent call last)
    <ipython-input-37-36a79d2a42b7> in <module>
         10 
         11 with foo() as f:
    ---> 12     raise Exception()
         13     print(f)
    
    Exception: 

    上面这么做有什么意义呢?

    当yield发生处为生成器函数增加了上下文管理,这是为函数增加上下文机制的方式。

    • 把yield之前的当做__enter__方法执行
    • 把yield之后的当做__exit__方法执行
    • 把yield的值作为__enter__的返回值
    import contextlib
    import time
    import datetime
    
    @contextlib.contextmanager
    def add(x,y):#为生成器函数增加了上下文管理
        start= datetime.datetime.now()
        try:
            yield x+y#yield5,yield的值只能有一个,作为__enter__方法的返回值
        finally:
            delta = (datetime.datetime.now()-start).total_seconds()
    
    with add(4,5) as f:
        #raise Exception()
        time.sleep(2)
        print(f)
    
    结果为:
    9

    总结:

    如果业务逻辑简单可以使用函数加contextlib.contextmanager装饰器方式,如果业务复杂,用类的方式加__enter__和__exit__方法方便。

     
     
     
     
     
     
  • 相关阅读:
    js去除空格
    Quartz定时任务学习(九)Quartz监听器
    Quartz定时任务学习(七)Cron 触发器
    数据挖掘之聚类算法K-Means总结
    SQL SERVER分区具体例子详解
    基于basys2驱动LCDQC12864B的verilog设计图片显示
    图像处理的多线程计算
    三维空间中的几种坐标系
    2017年要学习的三个CSS新特性
    Mesos 资源分配
  • 原文地址:https://www.cnblogs.com/xpc51/p/11792086.html
Copyright © 2011-2022 走看看