python --上下文管理器实现


前言

with语句的使用给我们带来了,很多的便利,最常用的就是关闭一个文件,释放一把锁。

实现原理

根据with的实现原理“上下文管理器协议”

此协议,提示我们要在类中去实现两个特族方法,enter(self)exit(self, exc_type, exc_val, exc_tb)
有了这两个方法,就可以使用with语句了,执行过程:先执行enter()方法, 然后执行添加在with下的代码,
最后执行exit()方法。

如果在执行过程中出现了异常,那么会自动执行exit()方法:

# -*-coding: utf-8-*-
"""
Context Manager:
    to implement 'with' in python.
"""

class ErrorProduce(object):
    def __init__(self) -> None:
        pass
    
    def produce(self):
        raise RuntimeError('Error: running time error!!!')

class MyWith(object):
    def __enter__(self):
        return ErrorProduce()
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type:
            print(exc_type)
            print("error!")
            # print(exc_val)  #  放开之后,打印异常的值。
            # return True
        else:
            print("done.")
            

with MyWith() as E:
    E.produce()

结果:
error!
Traceback (most recent call last):
File "d:\workplace\project\machine-project\work\AI-project\context_manager.py", line 29, in
E.produce()
File "d:\workplace\project\machine-project\work\AI-project\context_manager.py", line 12, in produce
raise RuntimeError('Error: running time error!!!')
RuntimeError: Error: running time error!!!

由结果可知,会先打印‘error!’,然后打印相关错误,再程序由于错误终止,即产生了错误之后,还是执行了exit中的代码。
如果将exit中的注释放开,再次运行,程序将不会报错,但是会将exc_val中的错误信息打印出来,相当于exit方法将错误抹除了
抹除方式返回True,但是也将with中抛出错误后的代码都跳过了。

使用函数实现

引入contextlib模块中的contextmanage装饰器, 装饰器使函数可以使用with语句。

from contextlib import contextmanager

class ErrorProduce:
    def __init__(self) -> None:
        pass
    
    def produce(self):
        raise RuntimeError("ERROR")


@contextmanager
def my_with():
    try:
        yield ErrorProduce()
    except Exception as e:
        print(e)
    finally:
        print("收尾")
        

with my_with() as E:
    E.produce()
    print('aa')

结果:

ERROR
收尾

函数中使用了yield, 为一个生成器, 通过yield 来划分相当于类中的enter和exit, yield返回值可以当作as后的值
上段代码中我们发现使用了try,except,finally异常处理的语句,这些语句是必要的吗?

通过测试,如果没有这些异常处理,那么with语句还是可以执行的,这里的意思是可以使用with语句,
不会报不能使用with的错误,但是使用后,只有with下的语句中没有抛出异常,那么yield语句后边的代码才会执行,
一旦有异常抛出,仍然是程序运行终止。所以,一般还是要配合try,except,finally语句来构造。

还有一点要注意,使用这种方式,异常被except捕捉后,那么这个异常就失效了,
一般的我们还是会需要在except中再次将捕捉的异常抛出,也就是在后面加上raise,
因为我们使用with只是为了做好一个收尾,如关闭文件句柄。
或者另一种方式,我们干脆就不写except语句,直接写finally语句了。