流畅的python笔记(十五)上下文管理器和else块

本文深入探讨Python中的流程控制结构,包括for/else、while/else和try/else的使用,强调它们在不同场景下的语义。此外,详细介绍了上下文管理器和with语句,阐述其在资源管理中的重要性,并通过contextlib模块中的工具简化上下文管理器的创建。文章还展示了如何使用@contextmanager装饰器将生成器函数转换为上下文管理器,以实现更简洁的代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

前言

一、if语句之外的else块

for/else

try/else

两种变成风格:EAFP与LBYL

二、上下文管理器和with块

三、contextlib模块中的使用工具

四、使用@contextmanager


前言

本章讨论python中的流程控制特性:

  • with语句和上下文管理器,with语句会设置一个临时的上下文,交给上下文管理器对象控制,并且负责清理上下文。
  • for、while、try语句的else子句。

一、if语句之外的else块

else子句不仅能在if语句中使用,还能在for、while、try语句中使用,但在这些语句中使用时与if/else中的语义差别很大:

  • for/else。仅当for循环运行完毕时(即for循环没有被break语句中止),才运行else模块。
  • while/else。仅当while循环因为循环条件为假值而退出时(即while循环没有被break语句中止),才运行else块。
  • try/else。仅当try块中没有异常抛出时才运行else块,且else子句抛出的异常不会由前面的except子句处理。

在所有情况下,只要异常或者return、break、continue语句导致控制权跳到了复合语句的主块之外,else子句也会被跳过。

for/else

如下例子,如果找到了'banana',则break会导致控制权跳出for循环,则else子句不会执行。

try/else

不使用else的情况:

上述代码中只有当dangerous_call()不抛出异常的时候,after_call才会执行,但是这样代码结构不够清晰,try块中应该只放有可能抛出异常的语句,因此像下面这样用try/else版本更好:

现在这样很明确,try块防守的是dangerous_call可能出现的错误,而不是防守after_call。而且逻辑正确,只有当dangerous_call不抛出异常时,才会执行after_call()。

两种变成风格:EAFP与LBYL

EAFP:easier to ask for forgiveness than permission,取得原谅比获得许可容易。即先假定可以正常执行,如果假定不成立,即抛出了异常,那么捕获异常进行处理。需要使用很多的try/except。

LBYL,look before you leap,三思而后行。通常用大量的if语句进行判断,判断成功才执行。

二、上下文管理器和with块

上下文管理器对象存在的目的是管理with语句,就像迭代器对象存在的目的是为了管理for语句。

        with语句的目的是简化try/finally模式。try/finally模式用于保证try模块中代码运行完毕后执行finally模块中的操作,即便try模块中代码由于异常、return语句或sys.exit()调用而中止,也会执行finally模块中的操作。finally子句中的代码通常用于释放重要的资源,或者还原临时变更的状态。

        上下文管理器协议包含__enter__和__exit__两个方法,只要实现了__enter__和__exit__这两个方法,就能作为with语句块中的上下文管理器对象使用。with语句开始运行时,会在上下文管理器对象上调用__enter__方法。with语句运行结束后,会在上下文管理器对象上调用__exit__方法,__exit__方法来扮演finally子句的角色。

        最常见的例子就是确保关闭文件对象。

  1. fp绑定到打开的文件对象上,因为文件的__enter__方法返回self。
  2. 从fp中读取一些数据。
  3. fp变量仍然可用,即与函数和模块不同,with块并没有定义新的作用域。
  4. 可以读取fp对象的属性。
  5. 但是不能在fp上执行I/O操作,因为在with块的末尾,调用TextIOWrapper.__exit__方法把文件关闭了。

标号1处,执行with语句后边的open('mirror.py')得到的结果是上下文管理器对象(这里就是文件对象,本质是TextTOWrapper类的实例(open()函数返回类型),其__enter__方法返回self),在上下文管理器对象上调用__enter__方法使得文件对象本身被绑定在了as子句中的fp上,因为文件对象的__enter__方法返回的是self。即with把with块对象的__enter__方法的返回值绑定到as自己的fp上

        不管控制流程以哪种方式退出with块,都会在上下文管理器对象(上边例子中即open函数返回的TextIOWrapper对象)调用__exit__方法,而不是在__enter__方法返回的对象上调用(但是上边例子中上下文管理器对象与__enter__返回的对象是同一个)。

        with语句的as子句是可选的,对open函数来说,必须加上as子句,以便获取文件的引用。不过有些上下文管理器会返回None,因为没什么有用的对象能提供给用户。

        下边例子在一个精心制作的上下文管理器对象上执行操作,以此强调上下文管理器与__enter__方法返回的对象之间的区别。

  1. 上下文管理器是LookingGlass类的实例,python在上下文管理器上调用__enter__方法,把返回结果绑定到what上。
  2. 打印一个字符串值,然后打印一个what变量的值。
  3. 打印出的内容是反向的,两个print语句打印的都是反向的。
  4. 现在with块已经执行完毕,可以看出,__enter__方法返回的值,即存储在what变量中的值,是字符串‘JABBERWOCKY’。
  5. 现在用print打印也不再是反向的。

下面是LookingGlass上下文管理类的代码实现:

  1. 除了self之外,python调用__enter__方法时不传入其他参数。
  2. 把sys.stdout.write方法保存在实例属性self.original_write中,以供后边使用。
  3. 为sys.stdout.write打猴子补丁,替换成自己编写的方法。
  4. 返回‘JABBERWOCKY’字符串,用于存入目标变量what。
  5. 用于打猴子补丁的方法,返回来打印。
  6. 如果一切正常,python调用__exit_-方法时传入的参数是None,None,None,如果抛出了异常,这三个参数是异常数据。
  7. 重复代入模块不会消耗很多资源,因为python会缓存导入的模块。
  8. 还原成原来的sys.stdout_write方法。
  9. 如果exc_type不是None,即捕获到了异常,且是ZeroDivisionError类型,则打印错误消息...
  10. 然后返回True,告诉解释器,异常已经被处理。
  11. 如果__exit__方法返回None,或者True之外的值,with块中的任何异常都会向上冒泡。

解释器调用__enter__方法时,除了隐式的self之外,不会传入任何参数,传给__exit__方法的三个参数如下:

  1. exc_type,异常类,例如ZeroDivisionError。
  2. exc_value,异常实例,有时会有参数传给异常构造方法,例如错误消息,这些参数可以使用exc_value.args获取。
  3. traceback,trackback对象。

下面在with块之外测试上下文管理类,需要我们手动调用__enter__和__exit__方法:

  1. 实例化一个上下文管理器对象。
  2. 调用上下文管理器对象的__enter__方法,这个方法会用逆序打印来接管标准输出。并返回一个字符串给变量monster。
  3. monster字符串的值判断是对的,但是打印出的True是逆序的,因为调用__enter__的时候标准输出已经被打补丁接管了。
  4. 调用上下文管理器对象的__exit__方法,还原标准输出。

三、contextlib模块中的使用工具

contextlib模块提供一些使用工具,帮助创建上下文管理器类。

  • closing,如果对象提供了close()方法,但是没有实现__enter__/__exit__协议,那么可以使用这个函数构建上下文管理器。
  • suppress,构建临时忽略指定异常的上下文管理器。
  • @contextmanager,这个装饰器把简单的生成器函数变成上下文管理器,这样就不用创建类去实现管理器协议了。
  • ContexDecorator,这是个基类,用于定义基于类的上下文管理器,这种上下文管理器也能用于装饰函数,在受管理的上下文中运行整个函数。
  • ExitStack,这个上下文管理器能进入多个上下文管理器,with块结束时,ExitStack按照后进先出的顺序调用栈中各个上下文管理器的__exit__方法。如果事先不知道with块要进入多少个上下文管理器,可以使用这个类,比如同时打开任意一个文件列表中的所有文件。

这些工具中使用最广泛的是@contextmanager装饰器,其有迷惑人的一面,即它与迭代无关,却要使用yield语句,由此可以引出协程。

四、使用@contextmanager

@contextmanager装饰器能减少创建上下文管理器类的代码量,因为不用编写一个完整的类去实现__enter__和__exit__方法,而只需实现一个有yield语句的生成器,生成想让__enter__方法返回的值。

        在使用@contextmanager装饰的生成器中,yield语句的作用是把函数的定义体分为两部分:

  1. yield语句前面的所有代码在with块开始,即解释器调用__enter__方法时执行。
  2. yield语句后边的代码在with块结束时,即调用__exit__方法时执行。

  1. 应用contextmanager装饰器。
  2. 保存原来的sys.stdout.write方法。
  3. 定义自定义的reverse_write方法。
  4. 把sys.stdout.write替换成reverse_write。
  5. 产出一个值,即__enter__方法的返回值,这个值会绑定到with语句中as子句的目标变量上,执行with块中的代码时,这个函数会在这一点暂停。
  6. 恢复标准输出,这里是__exit__方法的代码,当控制权一旦跳出with块,就会继续执行yield语句之后的代码。

实际上,contextlib.contextmanager装饰器会把函数包装成实现了__enter__和__exit__方法的类。

类的名称是_GnegeratorContextManager,其__enter__方法有如下作用:

  1. 调用生成器函数,保存生成器对象gen。
  2. 调用next(gen),执行到yield关键字所在的位置。
  3. 返回next(gen)产出的值,以便把产出的值绑定到with/as语句中的目标变量上。

with块终止时,__exit__方法会做以下几件事:

  1. 检查有没有异常传给exc_type,如果有,调用gen.throw(exception),在生成器函数定义体中包含yield关键字的那一行抛出异常。
  2. 否则,调用next(gen),继续执行生成器函数定义体中yield语句之后的代码。

上方使用生成器实现的上下文管理器有一个严重错误:如果在with块中抛出了异常,python解释器会将其捕获,然后在looking_glass函数的yield表达式里再次抛出。但是,哪里没有处理错误的代码,因此looing_glass函数会终止,永远无法恢复成原来的sys.stdout.write方法。下面例子解决这个问题:

  1.  创建一个变量,用于保存可能出现的错误消息。
  2.  处理ZeroDivisionError异常,设置一个错误消息。
  3.  撤销对sys.stdout.wirte方法做的猴子补丁。
  4.  如果设置了错误消息,就打印出来。

 

 

 

 

        

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值