Python学习笔记32:else块与上下文管理器

Python学习笔记32:else块与上下文管理器

本系列文章的代码都存放在Github项目:python-learning-notes

else块

else块在常见的编程语言中没有什么好说的,都是和if语句固定搭配出现,用途也一目了然,但在Python中有一些奇特的额外用途。

while与else块

我们来看这个例子:

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块内的程序。

也就是说如果whileelse一一起使用,有三种情况:

  • 没有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块

forelse块一同使用的情况与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学习笔记10:上下文协议中我们就已经介绍过上下文管理器,具体来说介绍的是如何用类实现一个上下文管理器。

除了那种结构清晰、容易理解和实现的方式以外,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
@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语句这个地方向上抛出,相应的,我们在这里加上异常捕获和处理就不会影响清理工作的正常执行了。

因为上下文管理器之前已经讨论过,所以本篇博客异常短小。

谢谢阅读。

else块和上下文管理器

错误订正:

  • 5/15订正:else块中的相关用途描述有误,已经订正,并加入了示例程序进行验证。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值