网站首页 文章专栏 上下文管理器
上下文管理器
创建于:2021-07-04 08:19:03 更新于:2024-05-06 15:05:16 羽瀚尘 339

上下文管理器

从资源的释放说起

我们知道在打开一个文件后必须关闭、打开一个socket之后也必须关闭,但是总会由于代码的比较复杂或其他原因忘记释放这些资源,导致未定义的后果。

关闭这些资源其实就是为了给后续代码一个“未曾破坏”的运行环境,即在使用这些资源的前后,应保证上下文环境是相同的。与嵌入式编程中的中断需要保存现场、恢复现场有些相似。

在python中是用with语句来实现上下文管理的。

with语句的执行流程

在python中使用with进行上下文的管理,with语句的执行过程如下:

  1. 计算表达式的值,返回一个上下文管理器对象
  2. 加载上下文管理器对象的exit()方法,但不执行
  3. 调用上下文管理器对象的enter()方法
  4. 如果with语句设置了目标对象,则将enter()方法的返回值赋给目标对象
  5. 执行with中的代码块
  6. 如果5中的代码正常结束,调用上下文管理器对象的exit()方法,其返回值直接忽略。
  7. 如果5中的代码发生异常,调用上下文管理器对象的exit()方法,并将异常类型、异常值和traceback传递给exit()方法。如果exit()方法返回值为false,则异常会被重新抛出;如果其返回值为true,则视为异常已经被处理,程序继续执行。

使用类实现上下文

在类中是通过__enter__()__exit__()方法实现的。下面是一个简单的例子。

class my_context():
    def __init__(self, num):
        self.num = num 
    def __enter__(self):
        print("entering")
        return self 
    def __exit__(self, exception_type,
                       exception_value,
                       traceback):
        print("exiting") 

## test it
with my_context(1) as ins:
    print("ins' num is", ins.num)

运行上述代码后,输出为

entering
ins' num is 1
exiting

可以看出,只要在类中实现了__enter____exit__这两个方法,就可以实现一个简单的上下文管理。

在类实现的上下文管理器中进行异常处理

class my_context():
    def __init__(self, num):
        self.num = num 
    def __enter__(self):
        print("entering")
        return self 
    def __exit__(self, exception_type,
                       exception_value,
                       traceback):
        print("exiting") 
        if exception_type is None:
            ret = True 
        elif exception_type is ValueError:
            print("Value error handled")
            ret = True 
        else:
            print("Unknown exception type,"
                  "throw it")
            ret = False 
        return ret 
## test it
print("Test 1, no exception")
with my_context(1) as ins:
    print("ins' num is", ins.num)

print()
print("Test 2, value error")
with my_context(2) as ins:
    print("ins' num is", ins.num)
    raise(ValueError)

print()
print("Test 3, key error")
with my_context(3) as ins:
    print("ins' num is", ins.num)
    raise(KeyError)

上述代码执行后输出如下:

Test 1, no exception
entering
ins' num is 1
exiting

Test 2, value error
entering
ins' num is 2
exiting
Value error handled

Test 3, key error
entering
ins' num is 3
exiting
Unknown exception type,throw it
Traceback (most recent call last):
  File "error_class.py", line 36, in <module>
    raise(KeyError)
KeyError

可以看到,如果在执行with代码块的时候发生了异常,可以在__exit__()方法中进行处理。如果处理结束,返回True,代码继续执行;如果无法处理,就返回False,python会把这个异常继续抛出,直至被正常处理。

使用生成器实现上下文管理器

如果我们只是为一个简单的函数进行上下文管理,那么定义一个类略有些麻烦。好在我们还有标准库可以使用,这个标准库是contextlib。下面是一个简单的应用例子。

from contextlib import contextmanager

@contextmanager
def process(num):
    print("entering")
    yield num 
    print("exiting")

## test it
with process(1) as test_num:
    print("test num is", test_num)

执行上述代码后,运行结果如下:

entering
test num is 1
exiting

可以看出,yield关键词用于产生一个生成器,这个生成器又被上下文管理器封装,最后由为with语句返回,即test_num.

在生成器实现的上下文管理器中进行异常处理
使用类的方法进行上下文管理时,异常是作为参数传递的,那使用生成器进行上下文管理时应该怎样做呢?首先想到用try...except语句,如下面的代码所示:

from contextlib import contextmanager

@contextmanager
def process(num):
    print("entering")
    try:
        yield num 
    except RuntimeError as err:
        print("handled error:", err)
    finally:
        print("exiting")

## test it
print("Test with runtime error")
with process(1) as test_num:
    print("test num is", test_num)
    raise(RuntimeError("It's runtime error"))

print() 
print("Test with value error")
with process(2) as test_num:
    print("test num is", test_num)
    raise(ValueError("It's value error"))

上述代码运行后,输出为:

Test with runtime error
entering
test num is 1
handled error: It's runtime error
exiting

Test with value error
entering
test num is 2
exiting
Traceback (most recent call last):
  File "./generator_error_func.py", line 24, in <module>
    raise(ValueError("It's value error"))
ValueError: It's value error

我们在try..except语句中对RuntimeError进行了处理,所以代码可以继续执行;没有对ValueError处理,所以异常继续向上抛,直到控制台输出错误信息。

这里也可以看出,实际上with代码块的内容是在紧接着yield关键词后执行的。了解这个执行顺序后,就可以对上下文管理中出现的错误进行处理。