问题来源
一直以来都只是在打开文件的时候使用到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行判断了实际代码抛出的异常是否为断言异常的子类,以此来实现断言一个特定的异常。