python中with语句背后的上下文管理器

问题来源

一直以来都只是在打开文件的时候使用到with语句,示例如下:

1 with open(filename,'r') as file:
2      file.write('hello python')

最近在学习pytest的时候,其中的基础示例用到了with语句,示例内容如下:

当我们希望断言一个特定的异常时,利用raise工具来识别代码中的异常

(原文:Assert that a certain exception is raised Use the raises helper to assert that some code raises an exception)

 1 # content of test_sysexit.py
 2 import pytest
 3 
 4 
 5 def f():
 6   raise SystemExit(1)
 7 
 8 
 9 def test_mytest():
10   with pytest.raises(SystemExit):
11     f()

上面的例子中with语句后面的对象不是常见的open,所以本文用来记录with语句背后的上下文管理器ContextManager。

什么是上下文管理器?

  • 概念:实现了上下文协议的对象即为上下文管理器。
  • 上下文协议:类中实现__enter__、__exit__方法
  • 作用:用于资源的获取和释放,如文件操作、数据库连接;处理异常;

with语句的语法规则

1 #上下文表达式:with exp as var
2 #上下文管理器:exp
3 #上下文管理器返回的资源对象:var
4 #上下文管理器中执行的语句块:block
5 
6 with exp as var
7   block

 一个例子来观察with语句python内部的执行过程

 1 #-*-coding:utf-8-*-
 2 #Author:raychou
 3 '''
 4 一个简单的例子,掌握理解with的上下文管理器的执行顺序
 5 '''
 6 class MyContextManager():
 7     def __enter__(self):
 8         print('**********访问资源**********')
 9         return self
10 
11     def __exit__(self, exc_type, exc_val, exc_tb):
12         print('**********关闭资源**********')
13 
14     def innertest(self):
15         print('********内部具体代码********')
16 
17 
18 def outertest():
19     print('********外部具体代码********')
20 
21 
22 print('进入with代码段之前\n')
23 
24 with MyContextManager() as var:
25     print('**执行with block代码块之前**')
26     outertest()
27     var.innertest()
28     print('**执行with block代码块之后**')
29 
30 print('\n进入with代码段之后')

执行结果如下:

进入with代码段之前

**********访问资源**********
**执行with block代码块之前**
********外部具体代码********
********内部具体代码********
**执行with block代码块之后**
**********关闭资源**********

进入with代码段之后

由此我们可以知道,在编写上下文管理器时,可以将资源获取等放在__enter__中,在一系列操作完成后,可以将资源关闭写在__exit__ 中。

通过上面的语法规则分析以下代码

with pytest.raises(SystemExit):
  f()

由此我们可以得知pytest.raises()返回的为一个上下文管理器,查看源码如下:

 1 def raises(  # noqa: F811
 2     expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]],
 3     *args: Any,
 4     match: Optional[Union[str, "Pattern"]] = None,
 5     **kwargs: Any
 6 ) -> Union["RaisesContext[_E]", Optional[_pytest._code.ExceptionInfo[_E]]]:
 7     __tracebackhide__ = True
 8     for exc in filterfalse(
 9         inspect.isclass, always_iterable(expected_exception, BASE_TYPE)
10     ):
11         msg = "exceptions must be derived from BaseException, not %s"
12         raise TypeError(msg % type(exc))
13 
14     message = "DID NOT RAISE {}".format(expected_exception)
15 
16     if not args:
17         if kwargs:
18             msg = "Unexpected keyword arguments passed to pytest.raises: "
19             msg += ", ".join(sorted(kwargs))
20             msg += "\nUse context-manager form instead?"
21             raise TypeError(msg)
22         return RaisesContext(expected_exception, message, match)
23     else:
24         func = args[0]
25         if not callable(func):
26             raise TypeError(
27                 "{!r} object (type: {}) must be callable".format(func, type(func))
28             )
29         try:
30             func(*args[1:], **kwargs)
31         except expected_exception as e:
32             # We just caught the exception - there is a traceback.
33             assert e.__traceback__ is not None
34             return _pytest._code.ExceptionInfo.from_exc_info(
35                 (type(e), e, e.__traceback__)
36             )
37     fail(message)

我们可以得知raises方法返回了一个RaisesContext对象,找到RaisesContext的类源码如下:

 1 class RaisesContext(Generic[_E]):
 2     def __init__(
 3         self,
 4         expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]],
 5         message: str,
 6         match_expr: Optional[Union[str, "Pattern"]] = None,
 7     ) -> None:
 8         self.expected_exception = expected_exception
 9         self.message = message
10         self.match_expr = match_expr
11         self.excinfo = None  # type: Optional[_pytest._code.ExceptionInfo[_E]]
12 
13     def __enter__(self) -> _pytest._code.ExceptionInfo[_E]:
14         self.excinfo = _pytest._code.ExceptionInfo.for_later()
15         return self.excinfo
16 
17     def __exit__(
18         self,
19         exc_type: Optional["Type[BaseException]"],
20         exc_val: Optional[BaseException],
21         exc_tb: Optional[TracebackType],
22     ) -> bool:
23         __tracebackhide__ = True
24         if exc_type is None:
25             fail(self.message)
26         assert self.excinfo is not None
27         if not issubclass(exc_type, self.expected_exception):
28             return False
29         # Cast to narrow the exception type now that it's verified.
30         exc_info = cast(
31             Tuple["Type[_E]", _E, TracebackType], (exc_type, exc_val, exc_tb)
32         )
33         self.excinfo.fill_unfilled(exc_info)
34         if self.match_expr is not None:
35             self.excinfo.match(self.match_expr)
36         return True

类RaisesContext中实现了__enter__和__exit__,即符合上下文协议;

再回到文章开头的例子,可以看出类文件第27行判断了实际代码抛出的异常是否为断言异常的子类,以此来实现断言一个特定的异常。

猜你喜欢

转载自www.cnblogs.com/ray0228/p/11648344.html
今日推荐