在 Python 开发中,我们经常会使用到 with 语法块,例如在读写文件时,保证文件描述符的正确关闭,避免资源泄露问题。

你有没有思考过, with 背后是如何实现的?我们常常听到的上下文管理器究竟是什么?

这篇文章我们就来学习一下 Python 上下文管理器,以及 with 的运行原理。

with语法块

在讲解 with 语法之前,我们先来看一下不使用 with 的代码如何写?

我们在操作一个文件时,代码可以这么写:

  1. # 打开文件
  2. f = open('file.txt')
  3. for line in f:
  4. # 读取文件内容 执行其他操作
  5. # do_something...
  6. # 关闭文件
  7. f.close()
  8. 复制代码

这个例子非常简单,就是打开一个文件,然后读取文件中的内容,最后关闭文件释放资源。

但是,代码这么写会有一个问题:在打开文件后,如果要对读取到的内容进行其他操作,在这操作期间发生了异常,这就会导致文件句柄无法被释放,进而导致资源的泄露。

如何解决这个问题?

也很简单,我们使用 try ... finally 来优化代码:

  1. # 打开文件
  2. f = open('file.txt')
  3. try:
  4. for line in f:
  5. # 读取文件内容 执行其他操作
  6. # do_something...
  7. finally:
  8. # 保证关闭文件
  9. f.close()
  10. 复制代码

这么写的好处是,在读取文件内容和操作期间,无论是否发生异常,都可以保证最后能释放文件资源。

但这么优化,代码结构会变得很繁琐,每次都要给代码逻辑增加 try ... finally 才可以,可读性变得很差。

针对这种情况,我们就可以使用 with 语法块来解决这个问题:

  1. with open('file.txt') as f:
  2. for line in f:
  3. # do_something...
  4. 复制代码

使用 with 语法块可以完成之前相同的功能,而且这么写的好处是,代码结构变得非常清晰,可读性也很好。

明白了 with 的作用,那么 with 究竟是如何运行的呢?

上下文管理器

首先,我们来看一下 with 的语法格式:

  1. with context_expression [as target(s)]:
  2. with-body
  3. 复制代码

with 语法非常简单,我们只需要 with 一个表达式,然后就可以执行自定义的业务逻辑。

但是,with 后面的表达式是可以任意写的吗?

答案是否定的。要想使用 with 语法块,with 后面的的对象需要实现「上下文管理器协议」。

什么是「上下文管理器协议」?

一个类在 Python 中,只要实现以下方法,就实现了「上下文管理器协议」:

  • __enter__:在进入 with 语法块之前调用,返回值会赋值给 withtarget
  • __exit__:在退出 with 语法块时调用,一般用作异常处理

我们来看实现了这 2 个方法的例子:

  1. class TestContext:
  2. def __enter__(self):
  3. print('__enter__')
  4. return 1
  5. def __exit__(self, exc_type, exc_value, exc_tb):
  6. print('exc_type: %s' % exc_type)
  7. print('exc_value: %s' % exc_value)
  8. print('exc_tb: %s' % exc_tb)
  9. with TestContext() as t:
  10. print('t: %s' % t)
  11. # Output:
  12. # __enter__
  13. # t: 1
  14. # exc_type: None
  15. # exc_value: None
  16. # exc_tb: None
  17. 复制代码

在这个例子中,我们定义了 TestContext 类,它分别实现了 __enter____exit__ 方法。

这样一来,我们就可以把 TestContext 当做一个「上下文管理器」来使用,也就是通过 with TestContext() as t 方式来执行。

从输出结果我们可以看到,具体的执行流程如下:

  • __enter__ 在进入 with 语句块之前被调用,这个方法的返回值赋给了 with 后的 t 变量
  • __exit__ 在执行完 with 语句块之后被调用

如果在 with 语句块内发生了异常,那么 __exit__ 方法可以拿到关于异常的详细信息:

  • exc_type:异常类型
  • exc_value:异常对象
  • exc_tb:异常堆栈信息

我们来看一个发生异常的例子,观察 __exit__ 方法拿到的异常信息是怎样的:

  1. with TestContext() as t:
  2. # 这里会发生异常
  3. a = 1 / 0
  4. print('t: %s' % t)
  5. # Output:
  6. # __enter__
  7. # exc_type: <type 'exceptions.ZeroDivisionError'>
  8. # exc_value: integer division or modulo by zero
  9. # exc_tb: <traceback object at 0x10d66dd88>
  10. # Traceback (most recent call last):
  11. # File "base.py", line 16, in <module>
  12. # a = 1 / 0
  13. # ZeroDivisionError: integer division or modulo by zero
  14. 复制代码

从输出结果我们可以看到,当 with 语法块内发生异常后,__exit__ 输出了这个异常的详细信息,其中包括异常类型、异常对象、异常堆栈。

如果我们需要对异常做特殊处理,就可以在这个方法中实现自定义逻辑。

回到最开始我们讲的,使用 with 读取文件的例子。之所以 with 能够自动关闭文件资源,就是因为内置的文件对象实现了「上下文管理器协议」,这个文件对象的 __enter__ 方法返回了文件句柄,并且在 __exit__ 中实现了文件资源的关闭,另外,当 with 语法块内有异常发生时,会抛出异常给调用者。

伪代码可以这么写:

  1. class File:
  2. def __enter__(self):
  3. return file_obj
  4. def __exit__(self, exc_type, exc_value, exc_tb):
  5. # with 退出时释放文件资源
  6. file_obj.close()
  7. # 如果 with 内有异常发生 抛出异常
  8. if exc_type is not None:
  9. raise exception
  10. 复制代码

这里我们小结一下,通过对 with 的学习,我们了解到,with 非常适合用需要对于上下文处理的场景,例如操作文件、Socket,这些场景都需要在执行完业务逻辑后,释放资源。

contextlib模块

对于需要上下文管理的场景,除了自己实现 __enter____exit__ 之外,还有更简单的方式来做吗?

答案是肯定的。我们可以使用 Python 标准库提供的 contextlib 模块,来简化我们的代码。

使用 contextlib 模块,我们可以把上下文管理器当成一个「装饰器」来使用。

其中,contextlib 模块提供了 contextmanager 装饰器和 closing 方法。

下面我们通过例子来看一下它们是如何使用的。

contextmanager装饰器

我们先来看 contextmanager 装饰器的使用:

  1. from contextlib import contextmanager
  2. @contextmanager
  3. def test():
  4. print('before')
  5. yield 'hello'
  6. print('after')
  7. with test() as t:
  8. print(t)
  9. # Output:
  10. # before
  11. # hello
  12. # after
  13. 复制代码

在这个例子中,我们使用 contextmanager 装饰器和 yield配合,实现了和前面上下文管理器相同的功能,它的执行流程如下:

  1. 执行 test() 方法,先打印出 before
  2. 执行 yield 'hello'test 方法返回,hello 返回值会赋值给 with 语句块的 t 变量
  3. 执行 with 语句块内的逻辑,打印出 t 的值 hello
  4. 又回到 test 方法中,执行 yield 后面的逻辑,打印出 after

这样一来,当我们使用这个 contextmanager 装饰器后,就不用再写一个类来实现上下文管理协议,只需要用一个方法装饰对应的方法,就可以实现相同的功能。

不过有一点需要我们注意:在使用 contextmanager 装饰器时,如果被装饰的方法内发生了异常,那么我们需要在自己的方法中进行异常处理,否则将不会执行 yield 之后的逻辑。

  1. @contextmanager
  2. def test():
  3. print('before')
  4. try:
  5. yield 'hello'
  6. # 这里发生异常 必须自己处理异常逻辑 否则不会向下执行
  7. a = 1 / 0
  8. finally:
  9. print('after')
  10. with test() as t:
  11. print(t)
  12. 复制代码

closing方法

我们再来看 contextlib 提供的 closing 方法如何使用。

closing 主要用在已经实现 close 方法的资源对象上:

  1. from contextlib import closing
  2. class Test():
  3. # 定义了 close 方法才可以使用 closing 装饰器
  4. def close(self):
  5. print('closed')
  6. # with 块执行结束后 自动执行 close 方法
  7. with closing(Test()):
  8. print('do something')
  9. # Output:
  10. # do something
  11. # closed
  12. 复制代码

从执行结果我们可以看到,with 语句块执行结束后,会自动调用 Test 实例的 close 方法。

所以,对于需要自定义关闭资源的场景,我们可以使用这个方法配合 with 来完成。

contextlib的实现

学习完了 contextlib 模块的使用,最后我们来看一下 contextlib 模块是究竟是如何实现的?

contextlib 模块相关的源码如下:

  1. class _GeneratorContextManagerBase:
  2. def __init__(self, func, args, kwds):
  3. # 接收一个生成器对象 (方法内包含 yield 的方法就是一个生成器)
  4. self.gen = func(*args, **kwds)
  5. self.func, self.args, self.kwds = func, args, kwds
  6. doc = getattr(func, "__doc__", None)
  7. if doc is None:
  8. doc = type(self).__doc__
  9. self.__doc__ = doc
  10. class _GeneratorContextManager(_GeneratorContextManagerBase,
  11. AbstractContextManager,
  12. ContextDecorator):
  13. def __enter__(self):
  14. try:
  15. # 执行生成器 代码会运行生成器方法的 yield 处
  16. return next(self.gen)
  17. except StopIteration:
  18. raise RuntimeError("generator didn't yield") from None
  19. def __exit__(self, type, value, traceback):
  20. # with 内没有异常发生
  21. if type is None:
  22. try:
  23. # 继续执行生成器
  24. next(self.gen)
  25. except StopIteration:
  26. return False
  27. else:
  28. raise RuntimeError("generator didn't stop")
  29. # with 内发生了异常
  30. else:
  31. if value is None:
  32. value = type()
  33. try:
  34. # 抛出异常
  35. self.gen.throw(type, value, traceback)
  36. except StopIteration as exc:
  37. return exc is not value
  38. except RuntimeError as exc:
  39. if exc is value:
  40. return False
  41. if type is StopIteration and exc.__cause__ is value:
  42. return False
  43. raise
  44. except:
  45. if sys.exc_info()[1] is value:
  46. return False
  47. raise
  48. raise RuntimeError("generator didn't stop after throw()")
  49. def contextmanager(func):
  50. @wraps(func)
  51. def helper(*args, **kwds):
  52. return _GeneratorContextManager(func, args, kwds)
  53. return helper
  54. class closing(AbstractContextManager):
  55. def __init__(self, thing):
  56. self.thing = thing
  57. def __enter__(self):
  58. return self.thing
  59. def __exit__(self, *exc_info):
  60. self.thing.close()
  61. 复制代码

源码中我已经添加好了注释,你可以详细看一下。

contextlib 源码中逻辑其实比较简单,其中 contextmanager 装饰器实现逻辑如下:

  1. 初始化一个 _GeneratorContextManager 类,构造方法接受了一个生成器 gen
  2. 这个类实现了上下文管理器协议 __enter____exit__
  3. 执行 with 时会进入到 __enter__ 方法,然后执行这个生成器,执行时会运行到 with 语法块内的 yield
  4. __enter__ 返回 yield 的结果
  5. 如果 with 语法块没有发生异常,with 执行结束后,会进入到 __exit__ 方法,再次执行生成器,这时会运行 yield 之后的代码逻辑
  6. 如果 with 语法块发生了异常,__exit__ 会把这个异常通过生成器,传入到 with 语法块内,也就是把异常抛给调用者

再来看 closing 的实现,closing 方法就是在 __exit__ 方法中调用了自定义对象的 close,这样当 with 结束后就会执行我们定义的 close 方法。

使用场景

学习完了上下文管理器,那么它们具体会用在什么场景呢?

下面我举几个常用的例子来演示下,你可以参考一下结合自己的场景使用。

Redis分布式锁

  1. from contextlib import contextmanager
  2. @contextmanager
  3. def lock(redis, lock_key, expire):
  4. try:
  5. locked = redis.set(lock_key, 'locked', expire)
  6. yield locked
  7. finally:
  8. redis.delete(lock_key)
  9. # 业务调用 with 代码块执行结束后 自动释放锁资源
  10. with lock(redis, 'locked', 3) as locked:
  11. if not locked:
  12. return
  13. # do something ...
  14. 复制代码

在这个例子中,我们实现了 lock 方法,用于在 Redis 上申请一个分布式锁,然后使用 contextmanager 装饰器装饰了这个方法。

之后我们业务在调用 lock 方法时,就可以使用 with 语法块了。

with 语法块的第一步,首先判断是否申请到了分布式锁,如果申请失败,则业务逻辑直接返回。如果申请成功,则执行具体的业务逻辑,当业务逻辑执行完成后,with 退出时会自动释放分布式锁,就不需要我们每次都手动释放锁了。

Redis事物和管道

  1. from contextlib import contextmanager
  2. @contextmanager
  3. def pipeline(redis):
  4. pipe = redis.pipeline()
  5. try:
  6. yield pipe
  7. pipe.execute()
  8. except Exception as exc:
  9. pipe.reset()
  10. # 业务调用 with 代码块执行结束后 自动执行 execute 方法
  11. with pipeline(redis) as pipe:
  12. pipe.set('key1', 'a', 30)
  13. pipe.zadd('key2', 'a', 1)
  14. pipe.sadd('key3', 'a')
  15. 复制代码

在这个例子中,我们定义了 pipeline 方法,并使用装饰器 contextmanager 让它变成了一个上下文管理器。

之后在调用 with pipeline(redis) as pipe 时,就可以开启一个事物和管道,然后在 with 语法块内向这个管道中添加命令,最后 with 退出时会自动执行 pipelineexecute 方法,把这些命令批量发送给 Redis 服务端。

如果在执行命令时发生了异常,则会自动调用 pipelinereset 方法,放弃这个事物的执行。

总结

总结一下,这篇文章我们主要介绍了 Python 上下文管理器的使用及实现。

首先我们介绍了不使用 with 和使用 with 操作文件的代码差异,然后了解到使用 with 可以让我们的代码结构更加简洁。之后我们探究了 with 的实现原理,只要实现 __enter____exit__ 方法的实例,就可以配合 with 语法块来使用。

之后我们介绍了 Python 标准库的 contextlib 模块,它提供了实现上下文管理更好的使用方式,我们可以使用 contextmanager 装饰器和 closing 方法来操作我们的资源。

最后我举了两个例子,来演示上下文管理器的具体使用场景,例如在 Redis 中使用分布式锁和事物管道,用上下文管理器帮我们管理资源,执行前置和后置逻辑。

所以,如果我们在开发中把操作资源的前置和后置逻辑,通过上下文管理器来实现,那么我们的代码结构和可维护性也会有所提高,推荐使用起来。

想学习更多关于python的知识可以加我QQ:2955637827

Python进阶——什么是上下文管理器?的更多相关文章

  1. Python进阶(上下文管理器与with语句)

    /*上下文管理器必须有__enter__和__exit__方法*/ class MyResource: def __enter__(self): print('链接资源') return self / ...

  2. python的上下文管理器-1

    reference:https://zhuanlan.zhihu.com/p/26487659 来看看如何正确关闭一个文件. 普通版: def m1(): f = open("output. ...

  3. python2.7高级编程 笔记一(Python中的with语句与上下文管理器学习总结)

    0.关于上下文管理器上下文管理器是可以在with语句中使用,拥有__enter__和__exit__方法的对象. with manager as var: do_something(var) 相当于以 ...

  4. 翻译《Writing Idiomatic Python》(五):类、上下文管理器、生成器

    原书参考:http://www.jeffknupp.com/blog/2012/10/04/writing-idiomatic-python/ 上一篇:翻译<Writing Idiomatic ...

  5. python学习笔记4(对象/引用;多范式; 上下文管理器)

    ### Python的强大很大一部分原因在于,它提供有很多已经写好的,可以现成用的对象 21. 动态类型:对象/引用 对象和引用: 对象是储存在内存中的实体,对象名只是指向这一对象的引用(refere ...

  6. Python深入02 上下文管理器

    作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明.谢谢! 上下文管理器(context manager)是Python2.5开始支持的一种语 ...

  7. python 上下文管理器

    作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明.谢谢! 上下文管理器(context manager)是Python2.5开始支持的一种语 ...

  8. 【Python学习笔记】with语句与上下文管理器

    with语句 上下文管理器 contextlib模块 参考引用 with语句 with语句时在Python2.6中出现的新语句.在Python2.6以前,要正确的处理涉及到异常的资源管理时,需要使用t ...

  9. 流畅python学习笔记:第十五章:上下文管理器

    在开始本章之前,我们首先来谈谈try-excep..final模块.在Python中,进行异常保护的最多就是用try..except..final.首先来看下下面的代码.进行一个简单的除法运算.为了防 ...

随机推荐

  1. css3系列之详解border-image

     border-image border-image呢,是给 边框加上背景图片的.没错,就是平常那一小小条的边框,也能加图片. 参数: border-image-source border-image ...

  2. WebsitePanel密码解密

    WebsitePanel是一套Windows系统中的虚拟主机管理系统,可以同时管理多台服务器. 通过反编译该系统的dll发现该系统的密码加密方式可逆. 解密流程 1,获取密钥 密钥保存在  Enter ...

  3. 【不尽如人意的redisTemplete封装】

    线下项目里对spring redisTemplete进行了简单的封装,但是项目里关于其序列化的配置真的有点一言难尽: 可以看到这里用了JdkSerializationRedisSerializer去对 ...

  4. [Android systrace系列] 抓取开机过程systrace

    ------------------------------------------------------------------------- 这篇文章的小目标:了解抓取开机过程systrace的 ...

  5. Android面试题《思考与解答》11月刊

    又来更新啦,Android面试题<思考与解答>11月刊奉上. 说说View/ViewGroup的绘制流程 View的绘制流程是从ViewRoot的performTraversals开始的, ...

  6. Netty 心跳处理

    传统的心跳包设计,基本上是服务端和客户端同时维护 Scheduler,然后双方互相接收心跳包信息,然后重置双方的上下线状态表.此种心跳方式的设计,可以选择在主线程上进行,也可以选择在心跳线程中进行,由 ...

  7. 20191017_datatable.select() 数据源中没有dataRow

    filterStr =" 记录时间 >= '2019/10/17 00:00:00' and 记录时间 <='2019/10/20 23:59:59' " 代码: dg ...

  8. Python+moviepy使用manual_tracking和headblur函数10行代码实现视频人脸追踪打马赛克

    ☞ ░ 前往老猿Python博文目录 ░ 一.背景知识 1.1.headblur简介 追踪人脸打马赛克需要使用headblur函数. 调用语法: headblur(clip,fx,fy,r_zone, ...

  9. PyQt(Python+Qt)学习随笔:Qt Designer中部件mimimumSize和maximumSize的含义

    1.mimimumSize mimimumSize表示部件能被缩小到的最小尺寸,单位为像素,缩小到该尺寸后不能再进一步缩小了.如果部件在布局管理器中,且布局管理器也设置了最小尺寸,则部件本身的最小尺寸 ...

  10. ATT&CK 实战 - 红日安全 vulnstack (一) 靶机渗透

    关于部署:https://www.cnblogs.com/Cl0ud/p/13688649.html PS:好菜,后来发现内网主机还是PING不通VM1,索性三台主机全部配成NAT模式,按照WEB靶机 ...