python3,浅谈with的神奇魔法

发布时间:2019-08-05 15:33:48编辑:auto阅读(1401)

    在实际的编码过程中,有时有一些任务,需要事先做一些设置,事后做一些清理,这时就需要python with出场了,with能够对这样的需求进行一个比较优雅的处理,最常用的例子就是对访问文件的处理。

    一般访问文件资源时我们会这样处理:

    f = open(r'c:\test.txt', 'r')
    data = f.read()
    f.close()

    这样写没有错,但是容易犯两个毛病:
    1. 如果在读写时出现异常而忘了异常处理。
    2. 忘了关闭文件句柄

    以下的加强版本的写法:

    f = open(r'c:\test.txt', 'r')
    try:
        data = f.read()
    finally:
        f.close()

    以上的写法就可以避免因读取文件时异常的发生而没有关闭问题的处理了。代码长了一些。
    但使用with有更优雅的写法:

    with open(r'c:\test.txt', 'r') as f:
        data = f.read()

    说明:
    with后面接的对象返回的结果赋值给f。此例当中open函数返回的文件对象赋值给了f.with会自已获取上下文件的异常信息。
    with是如何做到的呢?
    with后面返回的对象要求必须两__enter__()/__exit__()这两个方法,而文件对象f刚好是有这两个方法的,故应用自如。
    pytho中官方定义说明如下(https://docs.python.org/2/reference/datamodel.html#context-managers):

    object.__enter__(self)
    进入与此对象相关的运行时上下文。with语句将将此方法的返回值绑定到语句的AS子句中指定的目标(如果有设置的话)
    
    object.__exit__(self, exc_type, exc_value, traceback)
    退出与此对象相关的运行时上下文。参数描述导致上下文退出的异常。如果上下文运行时没有异常发生,那么三个参数都将置为None。
    如果有异常发生,并且该方法希望抑制异常(即阻止它被传播),则它应该返回True。否则,异常将在退出该方法时正常处理。
    
    请注意, __exit__()方法不应该重新抛出传入的异常,这是调用者的职责。

    下面举例说明他的原理:

    1. 无异常发生时的例子:

    #!/user/bin/env python3
    #-*- coding:utf-8 -*-
    
    class Test:
        def __enter__(self):
            print('__enter__() is call!')
            return self
    
        def dosomething(self):
            print('dosomethong!')
    
        def __exit__(self, exc_type, exc_value, traceback):
            print('__exit__() is call!')
            print(f'type:{exc_type}')
            print(f'value:{exc_value}')
            print(f'trace:{traceback}')
            print('__exit()__ is call!')
    
    with Test() as sample:
        sample.dosomething()
    
    >>
    __enter__() is call!
    dosomethong!
    __exit__() is call!
    type:None
    value:None
    trace:None
    __exit()__ is call!

    以上的实例Text,我们注意到他带有__enter__()/__exit__()这两个方法,当对象被实例化时,就会主动调用__enter__()方法,任务执行完成后就会调用__exit__()方法,另外,注意到,__exit__()方法是带有三个参数的(exc_type, exc_value, traceback), 依据上面的官方说明:如果上下文运行时没有异常发生,那么三个参数都将置为None, 这里三个参数由于没有发生异常,的确是置为了None, 与预期一致. 

    2. 有异常发生时,会抛出异常的例子:
    以下例子在上面稍做了一些修改,让运行时产生异常,看看这三个参数的赋值情况:

    #!/user/bin/env python3
    #-*- coding:utf-8 -*-
    
    class Test:
        def __enter__(self):
            print('__enter__() is call!')
            return self
    
        def dosomething(self):
            x = 1/0
            print('dosomethong!')
    
        def __exit__(self, exc_type, exc_value, traceback):
            print('__exit__() is call!')
            print(f'type:{exc_type}')
            print(f'value:{exc_value}')
            print(f'trace:{traceback}')
            print('__exit()__ is call!')
            # return True
    
    
    with Test() as sample:
        sample.dosomething()
    >>
    __enter__() is call!
    Traceback (most recent call last):
    __exit__() is call!
    type:<class 'ZeroDivisionError'>
      File "C:/Users/xxx/PycharmProjects/Test1/test.py", line 23, in <module>
    value:division by zero
        sample.dosomething()
    trace:<traceback object at 0x000001C08CF32F88>
      File "C:/Users/xxx/PycharmProjects/Test1/test.py", line 10, in dosomething
    __exit()__ is call!
        x = 1/0
    ZeroDivisionError: division by zero

    从结果可以看出, 在执行到dosomethong时就发生了异常,然后将异常传给了__exit__(), 依据上面的官方说明:如果有异常发生,并且该方法希望抑制异常(即阻止它被传播),则它应该返回True。否则,异常将在退出该方法时正常处理。当前__exit__并没有写明返回True,故会抛出异常,也是合理的,但是正常来讲,程序应该是不希望它抛出异常的,这也是调用者的职责,我们将再次修改__exit__, 将其返回设置为True, 

    3. 有异常发生时,不再抛出异常的例子:

    在上面的例子上做点修改.
    #!/user/bin/env python3
    #-*- coding:utf-8 -*-
    
    class Test:
        def __enter__(self):
            print('__enter__() is call!')
            return self
    
        def dosomething(self):
            x = 1/0
            print('dosomethong!')
    
        def __exit__(self, exc_type, exc_value, traceback):
            print('__exit__() is call!')
            print(f'type:{exc_type}')
            print(f'value:{exc_value}')
            print(f'trace:{traceback}')
            print('__exit()__ is call!')
            return True
    
    
    with Test() as sample:
        sample.dosomething()
    
    >>
    __enter__() is call!
    __exit__() is call!
    type:<class 'ZeroDivisionError'>
    value:division by zero
    trace:<traceback object at 0x000001C94E592F88>
    __exit()__ is call!

    从结果看,异常抛出被抑制了,符合预期。
     

关键字