本系列文章的代码都存放在Github项目:。
else块
else
块在常见的编程语言中没有什么好说的,都是和if
语句固定搭配出现,用途也一目了然,但在Python中有一些奇特的额外用途。
我们来看这个例子:
import random
def roll():
return random.randint(1, 6)
i = 1
isWinner = False
while i <= 3:
rollResult = roll()
print("roll result is {!s}".format(rollResult))
if(rollResult == 6):
isWinner = True
break
i += 1
if isWinner:
print("you are a winner")
else:
print('you are a loser')
这是一个简单的掷骰子游戏,有三次机会,如果能掷出6点就是胜利者,控制台会输出获胜信息,否则显示失败信息。
这个例子可以用while/else
的形式改写:
import random
def roll():
return random.randint(1, 6)
i = 1
while i <= 3:
rollResult = roll()
print("rull result is {!s}".format(rollResult))
if(rollResult == 6):
print('you are a winner')
break
i += 1
else:
print("you are a loser")
这两段代码是等效的。
上面的示例中不难看出,else
搭配循环这种方式并非控制流程所必须的,没有它的时候我们完全可以用一些变量来标记循环体是否非正常退出,比如第一个例子中的isWinner
就起到了标记的作用。而如果使用了else
块,就可能会省略这些标记,因为从语句层面区分了流程控制。
但必须要指出的是这种方式反而比用变量标记更容易出错,如果你使用的不是很熟练的话。下面我们分析else
块和while
循环一起使用时候的具体情况。
else
块和while
循环一起使用的时候,它的作用是在循环体循环条件为False
(没有使用break
)时执行else
块内的程序。
也就是说如果
while
和else
一起使用,有三种情况:
没有
break
,循环遇到循环条件为False
时自动结束(包括使用continue
的情况),执行else
块。通过
break
结束循环,不执行else
块。通过抛出异常结束循环,不执行
else
块。
这里对这几种情况使用最精简的代码进行测试验证:
i = 1
while i <= 3:
print("->{!s}".format(i))
i += 1
else:
print("else is excuted")
# ->1
# ->2
# ->3
# else is excuted
i = 1
while i <= 3:
print("->{!s}".format(i))
i += 1
if i == 4:
continue
else:
print("else is excuted")
# ->1
# ->2
# ->3
# else is excuted
i = 1
while i <= 3:
print("->{!s}".format(i))
i += 1
if i == 2:
break
else:
print("else is excuted")
# ->1
i = 1
try:
while i <= 3:
print("->{!s}".format(i))
i += 1
if i == 2:
raise ZeroDivisionError
else:
print("else is excuted")
except ZeroDivisionError:
pass
# ->1
for与else块
for
与else
块一同使用的情况与while
类似,只有for
循环迭代完成后自动退出(包含使用了continue
语句的情况)时候,才会执行else
块。
其它诸如break
或者异常等原因跳出for
循环的,else
中的语句不会执行。
同样的,我们可以使用精简的代码进行验证:
iterator = range(1,4)
for i in iterator:
print("->{!s}".format(i))
else:
print("else block is called")
# ->1
# ->2
# ->3
# else block is called
iterator = range(1,4)
for i in iterator:
print("->{!s}".format(i))
if i == 3:
break
else:
print("else block is called")
# ->1
# ->2
# ->3
iterator = range(1,4)
for i in iterator:
print("->{!s}".format(i))
if i == 3:
continue
else:
print("else block is called")
# ->1
# ->2
# ->3
# else block is called
try:
iterator = range(1,4)
for i in iterator:
print("->{!s}".format(i))
if i == 3:
raise ZeroDivisionError
else:
print("else block is called")
except ZeroDivisionError:
pass
# ->1
# ->2
# ->3
try/except与else块
else
块与异常捕获语句一起使用时候的作用是:如果没有捕获到异常,则执行else
块。
老实说,一开始我觉得这有点脱了裤子放屁的嫌疑,因为比如这样:
def tryDetectFunc():
pass
def afterTryFunc():
pass
try:
tryDetectFunc()
afterTryFunc()
except Exception:
pass
try:
tryDetectFunc()
except Exception:
pass
else:
afterTryFunc()
我并不觉得这两段代码有什么区别,其中afterTryFunc()
都是只有在tryDetectFunc()
不会产生异常的情况下才会执行,但是仔细思索后你会发现这两者还真的有所不同,比如如果tryDetectFunc
没有异常,但afterTryFunc
有异常:
def tryDetectFunc():
pass
def afterTryFunc():
1/0
try:
tryDetectFunc()
afterTryFunc()
except Exception:
pass
try:
tryDetectFunc()
except Exception:
pass
else:
afterTryFunc()
# Traceback (most recent call last):
# File "D:\workspace\python\python-learning-notes\note32\test.py", line 16, in <module>
# afterTryFunc()
# File "D:\workspace\python\python-learning-notes\note32\test.py", line 4, in afterTryFunc
# 1/0
# ZeroDivisionError: division by zero
可以看到,第一段try
语句并没有报告异常,因为异常被except
捕获后“吞掉”了。但是第二段try
语句向上报告了异常,这是因为其位于else
块中,并不会被except
语句捕获。
老实说,这种差别在使用中反而很可能让人迷惑,进而产生一些bug。我也并没有看出这种写法的优点和用途。
总结
总的来说,当循环语句或者异常处理语句与else
块搭配使用的时候,else
块都是起到一种当执行结果符合预期(没有产生异常或者使用break
之类的手段终止控制流程)的时候,会执行else
中的语句。
显而易见的是此时的else
其字面意思和实际用途并不是很相符,所以Python社区有人呼吁这里应该使用类似then
之类的关键字,但是至少目前依然在使用古怪的else
。
上下文管理器
早在中我们就已经介绍过上下文管理器,具体来说介绍的是如何用类实现一个上下文管理器。
除了那种结构清晰、容易理解和实现的方式以外,Python还提供一种奇特的途径:使用装饰器、yield语句实现上下文管理器。
yield与上下文管理器
刚开始学习到这个我觉得有点匪夷所思,但是仔细一思考好像还真是那么回事。
我们仔细思考一下yield
语句有何特点:
def test_yield():
print('start')
yield 1
print('after')
ty = test_yield()
next(ty)
next(ty)
# start
# after
# Traceback (most recent call last):
# File "D:\workspace\python\python-learning-notes\note32\test.py", line 8, in <module>
# next(ty)
# StopIteration
如果像示例中那样,仅仅使用一个yield
语句的“生成器函数”,我们使用两次next
进行调用,第一次是执行yield
语句之前的程序,并通过yield
“生成”一个值,第二次调用是执行yield
语句之后的部分,然后抛出StopIteration
异常。
这个过程是不是和使用上下文管理协议的过程惊人相似?
同样是先做一些事情,并且获取一个在上下文中需要使用的句柄,在使用完上下文后,退出的时候再做一些清理工作,而这上面的示例几乎完全满足这整个过程。
当然,我们没法直接把这种简陋的“生成器函数”拿来当做上下文管理器使用,还需要做一些微小的工作,把它包装一下,这样Python解释器才会用的“舒服”。
import contextlib
contextmanager
.def test_yield():
print('start')
yield 1
print('after')
with test_yield() as hold:
print(hold)
print('do something')
# start
# 1
# do something
# after
这里我们需要用到的是contextlib
模块和contextlib.contextmanager
装饰器。
contextlib
模块是一个上下文管理器相关的模块,关于上下文管理器的组件都可以在这里找到。
contextmanager
装饰器可以把一个单yield
语句的“生成器函数”包装成一个上下文管理器,包装后的上下文管理器和通过类定义的上下文管理器的用法毫无二致。
这里的“生成器函数”是加了引号的,这是要提醒这里的使用了
yield
语句的函数并不是真正的生成器函数,其用途与生成器函数也没有任何关系,它的目的是为了实现一个上下文管理器。
此外还需要注意的是,并非所有的上下文管理器都会为上下文产生一个有用的句柄,示例中的yield 1
就几乎没有任何用处,如果上下文中不需要句柄的话,完全可以yield None
,而相应的,with
语句后也不需要使用as
给句柄命名。
现在似乎一切都很完美,我们用一个简单的函数轻松实现了一个上下文管理器。
但是如果回忆一下上下文协议的完整定义就会发现,在__exit__
方法中,关于异常的参数就占了一多半,所以上下文协议对于异常的处理是极为重要的。
仔细思考也能明白,上下文中的语句是完全由“用户”即兴发挥的,其中出现异常的概率很高,所以必须要考虑上下文中如果产生异常,上下文管理器如何处理,以及确保__exit__
中的清理动作能正常执行。
我们先测试一下不做任何改进的上下文管理器函数会出现什么情况:
import contextlib
@contextlib.contextmanager
def test_yield():
print('start')
yield 1
print('after')
with test_yield() as hold:
print(hold)
1/0
print('do something')
# start
# 1
# Traceback (most recent call last):
# File "D:\workspace\python\python-learning-notes\note32\test.py", line 10, in <module>
# 1/0
# ZeroDivisionError: division by zero
在上下文中我们加入了一个会产生除零错误的语句,结果也如同我们上边忧虑的,产生了一个异常,而清理动作(这里对应print('after')
)并没有成功执行。
解决方法也很简单:
import contextlib
@contextlib.contextmanager
def test_yield():
print('start')
try:
yield 1
except ZeroDivisionError:
print('division by zero')
print('after')
with test_yield() as hold:
print(hold)
1/0
print('do something')
# start
# 1
# division by zero
# after
只要把yield
语句包裹在异常捕获语句中就行了,这是因为一旦上下文中产生异常,就会由yield
语句这个地方向上抛出,相应的,我们在这里加上异常捕获和处理就不会影响清理工作的正常执行了。
因为上下文管理器之前已经讨论过,所以本篇博客异常短小。
谢谢阅读。
错误订正:
5/15订正:
else
文章评论