两篇文章看懂python上下文管理器with语法

文章一:转自python之禅

本文是作者无名小妖投稿的原创文章,文章结合示例代码对 with ... as 的来龙去脉做了详尽分析,我对其中部分内容有细微调整和排版处理。写博客不仅可以加深对知识的深入理解,还能提高你的语言组织能力。如果你喜欢写技术博客,想投稿可私信给我,文章采纳后会有 50~100元 的稿费鼓励。

说到 with 大家通常看到的应该是这样的:

示例 1

with open('courses.txt') as f:
   for i in f:
       print(i.strip())

打开一个文件,然后循环做一些事情。但是你知道为什么会有 with 吗?我们自己是不是能够写出可以作用在 with 关键字上的对象呢?

现在,我们带着上述两个问题来说一说 with 的由来以及上下文管理器相关内容。

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

示例1的功能我们可以使用 try/finally 模式实现:

示例2

try:
   f = open('courses.txt')
   for i in f:
       print(i.strip())
finally:
   f.close()

try中的 except 和 else 不是必须的,这里为了简单明了,我们只用了 finally。

对比两个示例,我们可以看到示例1相对简洁,这就是 with 的由来。

其实,语言中的一些特性或者说一些亮眼的特性,必然是有一个演化的过程的,我们作为后来者和使用者应该多花一些心思去思索其背后的实现过程,相信你会收获更多。

那么 上下文管理器 又是什么呢?

上下文管理器协议包含 __enter__ 和 __exit__ 两个方法。with 语句开始运行时,会在上下文管理器对象上调用 __enter__ 方法。with 语句运行结束后,会在上下文管理器对象上调用 __exit__ 方法,以此扮演 finally 子句的角色。最常见的例子是确保关闭文件对象(示例1)。

下面我们实现一个简单的例子来直观的感受一下:

class T:
   def __enter__(self):
       print('T.__enter__')
       return '我是__enter__的返回值'
   def __exit__(self, exc_type, exc_val, exc_tb):
       print('T.__exit__')

with T() as t:
   print(t)

输出:

T.__enter__
我是__enter__的返回值
T.__exit__

示例3中实现了一个类T,它的对象包含了__enter__和__exit__方法,有了这两个方法就可以使用 with 处理该对象。执行 with 后面的表达式T()得到的是上下文管理器对象,通过as字句把对象绑定到了变量t上。

观察输出结果,可以看到with块先调用了__enter__方法,在处理完内部逻辑(print(t))之后调用了exit方法,而t其实就是__enter__方法的返回值。

当然,这个例子只是为了方便我们理解上下文管理器,下面我们看一个更有意思的例子:

示例4

obj1 = HaHa('你手机拿反了')
with obj1 as content:
   print('哈哈镜花缘')
   print(content)
print('#### with 执行完毕后,再输出content: ####')
print(content)

输出:

缘花镜哈哈
了反拿机手你
#### with 执行完毕后,在输出content: ####
你手机拿反了

示例4中,上下文管理器是 HaHa 类的实例,Python 调用此实例的 __enter__ 方法,把返回结果绑定到 变量content 上。

打印一个字符串,然后打印 content 变量的值。可以看到打印出的内容都是是反向的。

最后,当 with 块已经执行完毕。可以看出,__enter__ 方法返回的值——即存储在 content 变量中的值——是字符串 ‘你手机拿反了’。
输出不再是反向的了。

HaHa类的实现:

import sys

class HaHa:

   def __init__(self, word):
       self.word = word

   def reverse_write(self, text):
       self.original_write(text[::-1])

   def __enter__(self):
       self.original_write = sys.stdout.write
       sys.stdout.write = self.reverse_write
       return self.word

   def __exit__(self, exc_type, exc_value, traceback):
       sys.stdout.write = self.original_write
       return True

在__enter__方法中,我们接管了标准输出,将其替换成我们自己编写的方法reverse_write,reverse_write方法将参数内容反转。而在__exit__方法中,我们将标准输出还原。__exit__方法需要返回True。

总之,with之于上下文管理器,就像for之于迭代器一样。with就是为了方便上下文管理器的使用。

上下文管理器特性在标准库中有一些应用:

  • 在 sqlite3 模块中用于管理事务;

  • 在 threading 模块中用于维护锁、条件和信号;


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

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

  1. yield 语句前面的所有代码在 with 块开始时(即解释器调用 __enter__ 方法时)执行

  2.  yield 语句后面的代码在with 块结束时(即调用 __exit__ 方法时)执行。

下面我们用 @contextmanager 装饰器来实现一下示例4的功能:

示例5

import sys
import contextlib

@contextlib.contextmanager
def WoHa(n):
   original_write = sys.stdout.write
   def reverse_write(text):
       original_write(text[::-1])
   sys.stdout.write = reverse_write
   yield n
   sys.stdout.write =  original_write
   return True

obj1 = WoHa('你手机拿反了')
with obj1 as content:
   print('哈哈镜花缘')
   print(content)
print('#### with 执行完毕后,在输出content: ####')
print(content)

输出:

缘花镜哈哈
了反拿机手你
#### with 执行完毕后,在输出content: ####
你手机拿反了

这里我们需要注意的是:代码执行到yield时,会产出一个值,这个值会绑定到 with 语句中 as 子句的变量上。执行 with 块中的代码时,这个函数会在yield这里暂停。此时,相当于示例4中执行完__enter__方法。而控制权一旦跳出 with 块(块内代码执行完毕)则继续执行 yield 语句之后的代码。

@contextmanager 装饰器优雅且实用,把三个不同的 Python 特性结合到了一起:函数装饰器、生成器和 with 语句。

现在,我想你应该能够解答开篇提到的两个问题了吧!

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

文章二:转自 python与数据分析

Python与数据分析   链接:

https://mp.weixin.qq.com/s/d5sPUA97sQ-GccFeqadsFA


with基本语法

Python老司机应该对下面的语法不陌生

with open('output', 'w') as f:
   f.write('Hello world')

上面的代码往output文件写入了Hello world字符串,with语句会在执行完代码块后自动关闭文件。这里无论写文件的操作成功与否,是否有异常抛出,with语句都会保证文件被关闭。

如果不用with,我们可能要用下面的代码实现类似的功能

try:
   f = open("output", "w")
   f.write("Hello world")
finally:
   f.close()

可以看到使用了with的代码比上面的代码简洁许多。


那么问题来了,上面的with代码背后发生了些什么?


我们来看下它的执行流程

  1. 首先执行open('output', 'w'),返回一个文件对象;

  2. 调用这个文件对象的__enter__方法,并将__enter__方法的返回值赋值给变量f;

  3. 执行with语句体,即with语句包裹起来的代码块;

  4. 不管执行过程中是否发生了异常,执行文件对象的__exit__方法,在__exit__方法中关闭文件。

这里的关键在于open返回的文件对象实现了__enter__和__exit__方法。一个实现了__enter__和__exit__方法的对象就称之为上下文管理器


上下文管理器

上下文管理器定义执行 with 语句时要建立的运行时上下文,负责执行 with 语句块上下文中的进入与退出操作。__enter__方法在语句体执行之前进入运行时上下文,__exit__在语句体执行完后从运行时上下文退出。

在实际应用中,__enter__一般用于资源分配,如打开文件、连接数据库、获取线程锁;__exit__一般用于资源释放,如关闭文件、关闭数据库连接、释放线程锁。


自定义上下文管理器

既然上下文管理器就是实现了__enter__和__exit__方法的对象,我们能不能定义自己的上下文管理器呢?答案是肯定的。

我们先来看下__enter__和__exit__方法的定义:

__enter__() - 进入上下文管理器的运行时上下文,在语句体执行前调用。如果有as子句,with语句将该方法的返回值赋值给 as 子句中的 target。

__exit__(exception_type, exception_value, traceback) - 退出与上下文管理器相关的运行时上下文,返回一个布尔值表示是否对发生的异常进行处理。如果with语句体中没有异常发生,则__exit__的3个参数都为None,即调用 __exit__(None, None, None),并且__exit__的返回值直接被忽略。如果有发生异常,则使用 sys.exc_info 得到的异常信息为参数调用__exit__(exception_type, exception_value, traceback)。出现异常时,如果__exit__(exception_type, exception_value, traceback)返回 False,则会重新抛出异常,让with之外的语句逻辑来处理异常;如果返回 True,则忽略异常,不再对异常进行处理。


理解了__enter__和__exit__方法后,我们来自己定义一个简单的上下文管理器。这里不做实际的资源分配和释放,而用打印语句来表明当前的操作。


class ContextManager(object):
   def __enter__(self):
       print("[in __enter__] acquiring resources")
   def __exit__(self, exception_type, exception_value, traceback):
       print("[in __exit__] releasing resources")
       if exception_type is None:
           print("[in __exit__] Exited without exception")
       else:
           print("[in __exit__] Exited with exception: %s" % exception_value)
           return False
with ContextManager():
   print("[in with-body] Testing")

运行上面的代码,会得到如下的输出

[in __enter__] acquiring resources
[in with-body] Testing
[in __exit__] releasing resources
[in __exit__] Exited without exception

我们在with语句体中人为地抛出一个异常

with ContextManager():
   print("[in with-body] Testing")
   raise(Exception("something wrong"))

会得到如下的输出

[in __enter__] acquiring resources
[in with-body] Testing
[in __exit__] releasing resources
[in __exit__] Exited with exception: something wrong
Traceback (most recent call last):
 File "/tmp/a.py", line 15, in <module>
   raise(Exception("something wrong"))
Exception: something wrong


如我们所期待,with语句体中抛出异常,__exit__方法中exception_type不为None,__exit__方法返回False,异常被重新抛出。

以上,我们通过实现__enter__和__exit__方法来实现了一个自定义的上下文管理器。

contextlib库

除了上面的方法,我们也可以使用contextlib库来自定义上下文管理器。如果用contextlib来实现,可以用下面的代码来实现类似的上下文管理器

from contextlib import contextmanager

@contextmanager
def func():
   try:
       print("[in __enter__] acquiring resources")
       yield
   finally:
       print("[in __exit__] releasing resources")

with func():
   print("[in with-body] Testing")
   raise(Exception("something wrong"))


上面的代码涉及到装饰器(@contextmanager),生成器(yield),有点难读。这里yield之前的代码相当于__enter__方法,在进入with语句体之前执行,yield之后的代码相当于__exit__方法,在退出with语句体的时候执行。


猜你喜欢

转载自blog.csdn.net/qq_27825451/article/details/80467500