只要对象内实现 enter() 和 exit() 方法,就能兼容 with 语句,触发上下文管理。
一、上下文管理的简单执行流程
with 工作原理 (1)紧跟with后面的语句被求值后,返回对象的“enter()”方法被调用,这个方法的返回值将被赋值给as后面的变量; (2)当with后面的代码块全部被执行完之后,将调用前面返回对象的“exit()”方法。
class Sample:
def enter(self):
print “in enter“
return “Foo”
def exit(self, exc_type, exc_val, exc_tb):
‘’’
若在执行流程中因为错误而退出,调用exit时,会自动捕获错误信息
exc_type: 错误的类型(异常类型)
exc_val: 错误类型对应的值 (异常值)
exc_tb: 代码中错误发生的位置 (错误栈)
‘’’
print “in exit“
def get_sample():
return Sample()
with get_sample() as sample:
print “Sample: “, sample
‘’’
流程总结:
1- 执行get_sample()函数
2- 函数内实例化Sample对象,执行enter返回字符串赋予sample变量
3- 执行with内的代码块,输出sample变量值
4- 执行Sample对象内的exit方法
‘’’
二、错误执行流程
class Sample():
def enter(self):
print(‘in enter’)
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print "type: ", exc_type
print "val: ", exc_val
print "tb: ", exc_tb
def do_something(self):
bar = 1 / 0
return bar + 10
with Sample() as sample:
sample.do_something()
‘’’
in enter
Traceback (most recent call last):
type:
val: integer division or modulo by zero
File “/home/user/cltdevelop/Code/TF_Practice_2017_06_06/with_test.py”, line 36, in
tb:
sample.do_something()
File “/home/user/cltdevelop/Code/TF_Practice_2017_06_06/with_test.py”, line 32, in do_something
bar = 1 / 0
ZeroDivisionError: integer division or modulo by zero
Process finished with exit code 1
‘’’
三、异常处理 with 应用
异常处理逻辑太多,以至于扰乱了代码核心逻辑。具体表现就是,代码里充斥着大量的 try、except、raise 语句,让核心逻辑变得难以辨识。
def upload_avatar(request):
“””用户上传新头像”””
try:
avatar_file = request.FILES[‘avatar’]
except KeyError:
raise error_codes.AVATAR_FILE_NOT_PROVIDED
try:
resized_avatar_file = resize_avatar(avatar_file)
except FileTooLargeError as e:
raise error_codes.AVATAR_FILE_TOO_LARGE
except ResizeAvatarError as e:
raise error_codes.AVATAR_FILE_INVALID
try:
request.user.avatar = resized_avatar_file
request.user.save()
except Exception:
raise error_codes.INTERNAL_SERVER_ERROR
return HttpResponse({})
这是一个处理用户上传头像的视图函数。这个函数内做了三件事情,并且针对每件事都做了异常捕获。如果做某件事时发生了异常,就返回对用户友好的错误到前端。
这样的处理流程纵然合理,但是显然代码里的异常处理逻辑有点“喧宾夺主”了。一眼看过去全是代码缩进,很难提炼出代码的核心逻辑。
早在 2.5 版本时,Python 语言就已经提供了对付这类场景的工具:“上下文管理器(context manager)”。上下文管理器是一种配合 with 语句使用的特殊 Python 对象,通过它,可以让异常处理工作变得更方便。
class raise_api_error:
“””
captures specified exception and raise ApiErrorCode instead
捕获指定的异常并改为引发ApiErrorCode
:raises: AttributeError if code_name is not valid
“””
def init(self, captures, code_name):
self.captures = captures
self.code = getattr(error_codes, code_name)
def __enter__(self):
# 刚方法将在进入上下文时调用
return self
def __exit__(self, exc_type, exc_val, exc_tb):
# 该方法将在退出上下文时调用
# exc_type, exc_val, exc_tb 分别表示该上下文内抛出的
# 异常类型、异常值、错误栈
if exc_type is None:
return False
if exc_type == self.captures:
raise self.code from exc_val
return False
在上面的代码里,我们定义了一个名为 raise_api_error 的上下文管理器,它在进入上下文时什么也不做。但是在退出上下文时,会判断当前上下文中是否抛出了类型为 self.captures 的异常,如果有,就用 APIErrorCode 异常类替代它。
使用该上下文管理器后,整个函数可以变得更清晰简洁:
def upload_avatar(request):
“””用户上传新头像”””
with raise_api_error(KeyError, ‘AVATAR_FILE_NOT_PROVIDED’):
avatar_file = request.FILES[‘avatar’]
with raise_api_error(ResizeAvatarError, 'AVATAR_FILE_INVALID'),\
raise_api_error(FileTooLargeError, 'AVATAR_FILE_TOO_LARGE'):
resized_avatar_file = resize_avatar(avatar_file)
with raise_api_error(Exception, 'INTERNAL_SERVER_ERROR'):
request.user.avatar = resized_avatar_file
request.user.save()
return HttpResponse({})
四、raies 和 raise……from 的区别
try:
… print(1 / 0)
… except:
… raise RuntimeError(“Something bad happened”)
…
Traceback (most recent call last):
File “
ZeroDivisionError: division by zero
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File “
RuntimeError: Something bad happened
try:
… print(1 / 0)
… except Exception as exc:
… raise RuntimeError(“Something bad happened”) from exc
…
Traceback (most recent call last):
File “
ZeroDivisionError: division by zero
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File “
RuntimeError: Something bad happened
不同之处在于,from 会为异常对象设置 cause 属性表明异常的是由谁直接引起的。
处理异常时发生了新的异常,在不使用 from 时更倾向于新异常与正在处理的异常没有关联。而 from 则是能指出新异常是因旧异常直接引起的。这样的异常之间的关联有助于后续对异常的分析和排查。from 语法会有个限制,就是第二个表达式必须是另一个异常类或实例。
如果在异常处理程序或 finally 块中引发异常,默认情况下,异常机制会隐式工作会将先前的异常附加为新异常的 _context _属性。
当然,也可以通过 with_traceback() 方法为异常设置上下文 context 属性,这也能在 traceback 更好的显示异常信息。