《流畅的Python》笔记。
本篇主要讨论Python用户常忽略掉的一些流程控制特性,包括上下文管理器和else块。内容包括else与非if关键字的搭配;Python中的上下文管理器,如何自定义上下文管理器,以及contextlib模块中@contextmanager装饰器的用法。
1. if语句之外的else块
else
除了和if
搭配之外,在Python中,它还能与for
,while
和 try
搭配:
for
:仅当for
循环运行完毕时才运行else
块while
:仅当while
循环因为条件为假而退出时才运行else
块try
:仅当try
块中没有抛出异常时才运行else
块,且else
块中抛出的异常不会被前面的except
子句处理- 在上述三个情况中,如果异常、
return
、break
或continue
语句导致控制权跳到了复合语句的主块之外,else
子句会被跳过。
在这些语句中使用else
字块有事能让代码更易读,而且能省去一些麻烦,不用设置控制标志或者添加额外的if
语句,尤其是在和try
复合时。try
块中的代码应该只含有预计会抛出异常的语句,以下是两种写法的对比:
# 代码1.1,只有dangerous_all()可能会抛出异常
# 写法1
try:
dangerous_all()
after_call()
except OSError:
log("OSError...")
# 写法2,此写法比上述写法更明确
try:
dangerous_all()
except OSError:
log("OSError...")
else: # 但其实这么写也是多余的
after_call()
复制代码
但是,并不建议大家在这些关键字后面加else
块,因为这很容易造成歧异,比如笔者第一眼看到for/else
时的理解是:如果不能进入for
块,则运行else
中的内容,但实际刚好相反。在其他语言中,此时的else
一般由关键字then
代替,但Python的创建人非常讨厌添加新关键字,所以让else
担起了这个职责。许多编程规范的书中也不建议在这些关键字后面添加else
块。
***补充:***在Python中,try/except
不仅用于错误处理,还和if/else
一样,常用于控制流程,因此,这就形成了两种代码风格:
- EAFP:“取得原谅比获得许可更容易”(Easier to Ask for Forgiveness than Permission),通俗讲就是“不管会不会抛异常,先运行再说,等抛出了异常再处理”,这种风格的特点就是代码中有很多
try/except
块; - LBYL:“三思而后行”(Look Before You Leap),这种风格就是显式测试前提条件,通俗讲就是“必须合规后才能运行”,这种风格的特点就是代码中有很多
if/else
块。
2. 上下文管理器和with块
说到上下文管理器,那首先就得说说什么是上下文。笔者第一次接触这个概念的时候很费解,笔者是按语文里的概念来理解的:不就是前一句话后一句话,前一段话后一段话吗,这有什么可管理的?虽然至今笔者也没看到关于“上下文”这个概念的准确定义,但用多了之后,大致能理解为:
某段代码B将整个程序分成了3段,从前到后分别为A,B,C。当运行代码段B时,程序运行环境的某些设定需要发生改变;当退出代码段B后,这些被改变的设置需恢复原样,即保持A和C的一致性。A和B,B和C就称之为上下文。由于某些原因(如程序员大意、抛出异常强制退出等),B中所改变的设置并不总能手动恢复回去,所以,通常将这些设置交由某些对象统一管理,这些对象就叫做上下文管理器。
2.1 Python中的上下文管理器
上下文管理器采用的是鸭子类型技术,实现了__enter__
和__exit__
两个抽象方法的对象就是上下文管理器。
上下文管理器对象的存在目的是为了管理with
语句,而with
语句的目的是简化try/finally
模式。
with
块的经典用法之一就是读写文件:
# 代码2.1
>>> with open("text.txt") as fp: # 变量fp还有一个称呼,叫"句柄"
... pass
...
>>> fp
<_io.TextIOWrapper name="text.txt" mode="r" encoding="UTF-8">
复制代码
解释:
with
后面的表达式(不包括as
部分)得到的结果就是一个上下文管理器。此处open()
函数返回了一个TextIOWrapper
对象,Python解释器会临时保存这个对象,我们这里将其取名为a
;- 在
with
语句块中,Python得到上下文管理器后会首先调用它的__enter__
方法,如果with
后面跟了as
关键字,则该方法的返回值会赋给as
后面的变量。上述代码中,当Python得到了a
后,调用它的__enter__
方法,该方法返回a
对象自身(return self
),然后变量fp
接收这个值。但请注意,并不是所有的上下文管理器的__enter__
都返回实例自身。 - 当退出
with
块时,Python会调用上下文管理器的__exit__
方法,做最后处理。上述代码中,Python并不是调用fp.__exit__()
,而是调用a.__exit__()
; - 与函数和模块不同,
with
块没有定义新的作用域,所以即便退出了with
块,变量fp
依然存在。
2.2 自定义上下文管理器
下面我们自定义一个上下文管理器来说明上述四条解释:
# 代码2.2
class LookingGlass:
def __enter__(self): # 该方法只要self一个参数
import sys
self.original_write = sys.stdout.write # 保存原方法
sys.stdout.write = self.reverse_write # 猴子补丁,临时替换原本的方法
return "JABBERWOCKY" # 并不一定是返回self!
def reverse_write(self, text):
self.original_write(text[::-1]) # 反转text内容
def __exit__(self, exc_type, exc_val, exc_tb): # 该方法有4个参数!
import sys # 由于Python会缓存导入的模块,重复导入不会消耗很多资源
sys.stdout.write = self.original_write # 恢复到原本的方法
if exc_type is ZeroDivisionError:
print("Please DO NOT divide by zero!")
return True # 返回True,表示异常已经正常处理
# 控制台中运行
>>> from mirror import LookingGlass
>>> with LookingGlass() as what:
... print("Alice, Kitty and SnowDrop")
... print(what)
...
porDwonS dna yttiK ,ecilA
YKCOWREBBAJ
>>> what
'JABBERWOCKY'
>>> print("Back to normal!")
Back to normal!
复制代码
解释:
-
__enter__
方法只有一个参数,即隐式的self
; -
__exit__
有四个参数,第一个参数是self
,其余三个参数主要用于处理with
块运行期间发生的异常,分别是:exc_type
:异常类exc_val
:异常实例,with
块中发生异常时抛出的对象。如果__exit__
想要向上抛出异常,那么在创建异常对象时传入的某些参数可从exc_val.args
中获取,比如错误信息。exc_tb
:traceback
对象。
如果
with
块中没有抛出异常,Python调用__exit__
方法时传入的参数是三个None
,否则传入异常数据。 -
当
with
块中发生异常时:如果__exit__
返回True
,表示异常已正确处理,Python解释器会压制异常;如果返回的是其它值,with
块中的任何异常都会向上冒泡。如果with
块中没有发生异常,则不用关注__exit__
的返回值。
2.3 contextlib模块
该模块包含了很多管理上下文的使用工具,下面列举出5个:
closing
:如果对象提供了close()
方法,但没有实现__enter__/__exit__
协议,则可以使用这个函数构建上下文管理器suppress
:构建临时忽略指定异常的上下文管理器@contextmanager
:这个装饰器很常用,它把简单的生成器函数变成上下文管理器,这样就不用创建类去实现管理器协议ContextDecorator
:这是个基类,用于定义基于类的上下文管理器。这种上下文管理器也能用于装饰函数,在受管理的上下文中运行整个函数ExitStack
:这个上下文管理器能保存多个上下文管理器。它是一个栈,with
结束时,依次调用栈中各个上下文管理器的__exit__
方法。如果事先不知道with
块要进入多少个上下文管理器,可以使用这个类。例如,同时打开任意一个文件列表中的所有文件。
2.4 @contextmanager
@contextmanager
装饰器能减少创建上下文管理器的样板代码量,不用编写一个完整的类,然后再实现__enter__
和__exit__
方法,而是只需实现一个仅含单个yield
语句的生成器,生成想让__enter__
方法返回的值。
在使用@contextmanager
装饰的生成器中,yield
语句的作用是把函数的定义体分成两部分:yield
语句前面的所有代码在with
块开始时(即解释器调用__enter__
方法时)执行,yield
之后的代码在with
块结束时(即调用__exit__
方法时)执行。
下面我们将之前的LookingGlass
类改写为生成器版本:
# 代码2.3
from contextlib import contextmanager
@contextmanager
def looking_glass():
import sys
original_write = sys.stdout.write
def reverse_write(text):
original_write(text[::-1])
sys.stdout.write = reverse_write
msg = ""
try:
yield "JABBERWOCKY" # 如果有异常,会在这里抛出
except ZeroDivisionError:
# 该装饰器默认所有异常都得到了处理,如果不想异常被压制,请在此处抛出
msg = "Please DO NOT divide by zero!"
finally:
sys.stdout.write = original_write
if msg:
print(msg)
# 用法和之前的版本一样:
>>> with looking_glass() as what: # 这里是唯一的变化
... print("Alice, Kitty and SnowDrop")
... print(what)
...
porDwonS dna yttiK ,ecilA
YKCOWREBBAJ
>>> what
'JABBERWOCKY'
复制代码
contextlib.contextmanager
装饰器会把函数包装成实现了__enter__
和__exit__
方法的类。
这个类的__enter__
方法有如下作用:
- 调用生成器函数,保存生成器对象(这里称其为
gen
) - 调用
next(gen)
,执行到yield
关键字所在的位置 - 返回
next(gen)
生成的值,将其绑定到with/as
语句中的目标变量上
它的__exit__
方法有如下作用:
- 检查有没有把异常传给
exc_type
;如果有,调用gen.throw(exception)
,在生成器函数定义体中yield
所在行抛出异常 - 否则,调用
next(gen)
,将生成器函数中剩余代码执行完。
前面说到,对一般的上下文管理器,如果with
中抛出了异常,Python解释器会根据__exit__
的返回值来决定是否压制异常。但@contextmanager
则不同:它提供的__exit__
方法默认所有异常都得到了处理。如果不想让@contextmanager
,必须在被装饰的函数中显式重新抛出异常。
3. 总结
本篇分为了两个部分,首先介绍了else
与for
、while
以及try
的搭配用法(但并不建议这么做,只需要知道能这么用就行了);随后是上下文管理器的内容,介绍了什么是“上下文”,什么是“上下文管理器”,Python中的上下文管理器以及with
块,然后我们自定义了一个上下文管理器,最后介绍了contextlib
模块,并用其中的@contextmanager
装饰器改写了自定义的上下文管理器。
迎大家关注我的微信公众号"代码港" & 个人网站 www.vpointer.net ~