with语句上下文管理器
官方定义
with 语句用于包装带有使用上下文管理器定义的方法的代码块的执行。 这允许对普通的 try…except…finally
使用模式进行封装以方便地重用。关于 with语句
的详细介绍见 PEP-343 。
执行过程
with
语句的执行过程如下:
- The context expression (the expression given in the
with_item
) is evaluated to obtain a context manager. - The context manager’s
__exit__()
is loaded for later use. - The context manager’s
__enter__()
method is invoked. - If a target was included in the with statement, the return value from
__enter__()
is assigned to it. - The suite is executed.
- The context manager’s
__exit__()
method is invoked. If an exception caused the suite to be exited, its type, value, and traceback are passed as arguments to__exit__()
. Otherwise, three None arguments are supplied.
在官方的中文文档中此部分翻译不是很精确,个人翻译如下(仅供参考)
- 使用
with语句
创建上下文管理器; - 创建上下文管理器时,自动加载
__exit__()
方法,供结束时自动调用; - 创建上下文管理器时,自动调用
__enter__()
方法; - 如果将
with语句
创建的上下文管理器赋值给某个变量名,则这个变量名的值是__enter__()
的返回值; - 执行上下文管理器中的代码段;
- 调用上下文管理器的
__exit__()
方法,如果在执行第5步的过程中代码报错,则会将错误类型、错误信息、回溯信息作为参数传递给__exit__()
方法,如未报错,则此三个参数均为None
值。
__enter__
上下文管理器入口,with语句
会自动将 __enter__()
方法的返回值赋值给 as
的 变量
。如果仅定义此方法,未定义 __exit__()
方法,将抛出 AttributeError
。
__exit__
上下文管理器自动退出时执行的方法,如果仅定义此方法,未定义 __enter__()
方法,将抛出 AttributeError
。
示例
话说作者养了一只橘猫,取名“呆呆”,俗话说“十只橘猫九只胖,还有一只压倒炕”,从它日常的习惯来看,绝对有成为最独特那只的资质。不过它在吃饱睡足之余,也会有一些有趣的行为,无聊的时候,在家里上蹿下跳,并伴随无聊的喵喵叫,当陪它玩耍一会后,它就躺在我腿上安心睡觉了。
将猫咪每次闹腾的行为,抽象为 CatBehavior
,如下示例,这样我们就可以使用 with语句
来管理猫咪单次活动的行为
class CatBehavior:
def __init__(self, name: str):
self.name = name
def __enter__(self):
pass
def __exit__(self, exc_type, exc_val, exc_tb):
pass
if __name__ == '__main__':
with CatBehavior('Tom') as behavior:
print(behavior)
上方示例中,输出的内容为 None
,这是因为 __enter__()
方法无返回值,现在我们来修改代码
class CatBehavior:
def __init__(self, name: str):
self.name = name
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print('exc_type: ', exc_type)
print('exc_val: ', exc_val)
print('exc_tb: ', exc_tb)
pass
def boring(self):
print(f'>>> {self.name}: Boring...')
def jump(self):
print(f'>>> {self.name}: Making noise...')
def played(self):
print(f'>>> {self.name}: Walk away after played...')
def sleep(self):
print(f'>>> {self.name}: Purr...purr...')
if __name__ == '__main__':
with CatBehavior('Tom') as behavior:
behavior.boring()
behavior.jump()
behavior.played()
behavior.sleep()
上方代码从逻辑上来说没有问题,但是从 面向对象
的角度来看,是有优化的点,优化如下
class CatBehavior:
def __init__(self, name: str):
self.name = name
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print('exc_type: ', exc_type)
print('exc_val: ', exc_val)
print('exc_tb: ', exc_tb)
pass
def boring(self):
print(f'>>> {self.name}: Boring...')
return self
def jump(self):
print(f'>>> {self.name}: Making noise...')
return self
def played(self):
print(f'>>> {self.name}: Walk away after played...')
return self
def sleep(self):
print(f'>>> {self.name}: Purr...purr...')
return self
if __name__ == '__main__':
with CatBehavior('Tom') as behavior:
behavior.boring().\
jump().\
played().\
sleep()
在实例的方法中返回实例本身,是一个很容易忽略的技巧,当然肯定不是所有的业务场景都适用这个技巧,还需要根据实际情况来实现。请注意:截止到现在,__exit__()
方法中参数的值均为 None
。
现在我们在 boring()
方法中抛出异常,代码修改如下
class CatBehavior:
# 仅修改boring方法,省略其他代码
def boring(self):
raise ValueError('Test error')
再次执行,我们会发现 __exit__()
方法中输出的内容如下,即代码中抛出的异常信息将作为参数传递给 __exit__()
方法,且程序报错退出
>>> exc_type: <class 'ValueError'>
>>> exc_val: Test error
>>> exc_tb: <traceback object at 0x0000013BE3D40E48>
在上面示例中,__exit__()
方法没有返回值,如果在此方法中返回 True
,报错信息将被忽略,修改 __exit__()
方法
即允许对普通的 try…except…finally 使用模式进行封装以方便地重用
class CatBehavior:
def __exit__(self, exc_type, exc_val, exc_tb):
return True
再次执行,可以看到程序正常执行完成,并未因异常而退出。
总结
我们可以通过实现 __enter__()
和 __exit__()
方法来让对象支持 with语句上下文管理器
,此特性较多用于对资源进行访问的场合,如访问文件、访问数据库等,当然也可以根据实际业务情况来自定义应用场景。