原文转载自「刘悦的技术博客」https://v3u.cn/a_id_217

在开发过程中,我们会经常面临的一个常见问题是如何正确管理外部资源,比如数据库、锁或者网络连接。稍不留意,程序将永久保留这些资源,即使我们不再需要它们。此类问题被称之为内存泄漏,因为每次在不关闭现有资源的情况下创建和打开给定资源的新实例时,可用内存都会减少。

正确管理资源往往是一个棘手的问题,因为资源的使用往往需要进行善后工作。善后工作要求执行一些清理操作,例如关闭数据库、释放锁或关闭网络连接。如果忘记执行这些清理操作,就可能会浪费宝贵的系统资源,例如内存和网络带宽。

背景

譬如,当开发人员使用数据库时,可能会出现一个常见问题是程序不断创建新连接而不释放或重用它们。在这种情况下,数据库后端可以停止接受新连接。这可能需要管理员登录并手动终止这些陈旧的连接,以使数据库再次可用。

以著名的ORM工具Peewee为例子:

pip3 install pymysql
pip3 install peewee

当我们声明数据库实例之后,试图链接数据库:

from peewee import MySQLDatabase  

db = MySQLDatabase('mytest', user='root', password='root',host='localhost', port=3306)  

print(db.connect())

程序输出:

True

但如果重复的创建链接:

from peewee import MySQLDatabase  

db = MySQLDatabase('mytest', user='root', password='root',host='localhost', port=3306)  

print(db.connect())
print(db.connect())

就会抛出异常:

Traceback (most recent call last):
File "/Users/liuyue/Downloads/upload/test/test.py", line 23, in <module>
print(db.connect())
File "/opt/homebrew/lib/python3.9/site-packages/peewee.py", line 3129, in connect
raise OperationalError('Connection already opened.')
peewee.OperationalError: Connection already opened.

所以,需要手动关闭数据库链接:

from peewee import MySQLDatabase  

db = MySQLDatabase('mytest', user='root', password='root',host='localhost', port=3306)  

print(db.connect())
print(db.close())
print(db.connect())

返回:

True
True
True

但这样操作有一个潜在的问题,如果在调用connect的过程中,出现了异常进而导致后续代码无法继续执行,close方法无法被正常调用,因此数据库资源就会一直被该程序占用而无法被释放。

继续改进:

from peewee import MySQLDatabase  

db = MySQLDatabase('mytest', user='root', password='root',host='localhost', port=3306)  

try:
print(db.connect())
except OperationalError:
print("Connection already opened.")
finally:
print(db.close())

改进后的逻辑是对可能发生异常的代码处进行OperationalError异常捕获,使用 try/finally 语句,该语句表示如果在 try 代码块中程序出现了异常,后续代码就不再执行,而直接跳转到 except 代码块。而最终,finally块逻辑的代码被执行。因此,只要把 close方法放在 finally 代码块中,数据库链接就会被关闭。

事实上,Peewee为我们提供了一种更加简洁、优雅的方式来操作数据库链接:

from peewee import MySQLDatabase  

db = MySQLDatabase('mytest', user='root', password='root',host='localhost', port=3306)  

with db.connection_context():
print("db is open")
print(db.is_closed())

也就是使用with 关键字来进行操作,这里使用with开启数据库的上下文管理器,当程序离开with关键字的作用域时,系统会自动调用close方法,最终效果和上文的捕获OperationalError异常一致,系统会自动关闭数据库链接。

上下文管理器(ContextManagers)

那么Peewee底层是如何实现对数据库的自动关闭呢?那就是使用Python3内置的上下文管理器,在Python中,任何实现了 __enter__() 和 __exit__() 方法的对象都可称之为上下文管理器,上下文管理器对象可以使用 with 关键字:

from peewee import MySQLDatabase  

db = MySQLDatabase('mytest', user='root', password='root',host='localhost', port=3306)  

class Db:  

    def __init__(self):  

        self.db = MySQLDatabase('mytest', user='root', password='root',host='localhost', port=3306)  

    def __enter__(self):  

        self.db.connect()  

    def __exit__(self,*args):  

        self.db.close()

__enter__() 方法负责打开数据库链接,__exit__() 方法负责处理一些善后工作,也就是关闭数据库链接。

藉此,我们就可以使用with关键字对Db这个类对象进行调用了:

with Db() as db:
print("db is opening") print(Db().db.is_closed())

程序返回:

db is opening
True

如此,我们就无需显性地调用 close方法了,由系统自动去调用,哪怕中间抛出异常 close方法理论上也会被调用。

上下文语法糖

Python3 还提供了一个基于上下文管理器的装饰器,更进一步简化了上下文管理器的实现方式。通过 生成器yield关键字将方法分割成两部分,yield 之前的语句在 __enter__ 方法中执行,yield 之后的语句在 __exit__ 方法中执行。紧跟在 yield 后面的值是函数的返回值:

from peewee import MySQLDatabase
from contextlib import contextmanager @contextmanager
def mydb():
db = MySQLDatabase('mytest', user='root', password='root',host='localhost', port=3306)
yield db
db.close()

随后通过with关键字调用contextmanager装饰后的方法:

with mydb() as db:
print("db is opening")

与此同时,Peewee也贴心地帮我们将此装饰器封装了起来:

from peewee import MySQLDatabase  

db = MySQLDatabase('mytest', user='root', password='root',host='localhost', port=3306)  

@db.connection_context()
def mydb():
print("db is open") mydb()

看起来还不错。

迷思:上下文管理一定可以善后吗?

请别太笃定,是的,上下文管理器美则美矣,但却未尽善焉,在一些极端情况下,未必能够善后:

from peewee import MySQLDatabase  

class Db:  

    def __init__(self):  

        self.db = MySQLDatabase('mytest', user='root', password='root',host='localhost', port=3306)  

    def __enter__(self):
print("connect")
self.db.connect()
exit(-1) def __exit__(self,*args):
print("close")
self.db.close() with Db() as db:
print("db is opening")

程序返回:

connect

当我们通过with关键字调用上下文管理器时,在__enter__方法内通过exit()方法强行关闭程序,过程中程序会立刻结束,并未进入到__exit__方法中执行关闭流程,也就是说,这个数据库链接并未被正确关闭。

同理,当我们书写了finally关键字,理所当然的,finally代码块理论上一定会执行,但其实,也仅仅是理论上:

def gen(text):
try:
for line in text:
try:
yield int(line)
except: pass
finally:
print('善后工作') text = ['1', '', '2', '', '3'] if any(n > 1 for n in gen(text)):
print('Found a number') print('并未善后')

程序返回:

Exception ignored in: <generator object gen at 0x100e177b0>
Traceback (most recent call last):
File "/Users/liuyue/Downloads/upload/test/test.py", line 71, in <module>
if any(n > 1 for n in gen(text)):
RuntimeError: generator ignored GeneratorExit
Found a number
并未善后

显而易见,当程序进入finally代码块之前,就立刻触发了一个生成器generator异常,当理论上要被捕获异常时程序被yield返回了原始状态,于是立刻退出,放弃了执行finally逻辑。

所以,逻辑上,我们并不能指望上下文管理器每一次都能够帮我们“善后”,至少,在事情尚未收束的情况下,能够随机应变:

from peewee import MySQLDatabase  

class Db:  

    def __init__(self):  

        self.db = MySQLDatabase('mytest', user='root', password='root',host='localhost', port=3306)  

    def __enter__(self):  

        if self.db.is_closeed():
print("connect")
self.db.connect() def __exit__(self,*args):
print("close")
self.db.close() with Db() as db:
print("db is opening") print(Db().db.is_closed())

结语

使用With关键字操作上下文管理器可以更快捷地管理外部资源,同时能提高代码的健壮性和可读性,但在极端情况下,上下文管理器也并非万能,还是需要诸如轮询服务等托底保障方案。

原文转载自「刘悦的技术博客」 https://v3u.cn/a_id_217

承上启下继往开来,Python3上下文管理器(ContextManagers)与With关键字的迷思的更多相关文章

  1. with与上下文管理器

    如果你有阅读源码的习惯,可能会看到一些优秀的代码经常出现带有 "with" 关键字的语句,它通常用在什么场景呢? 对于系统资源如文件.数据库连接.socket 而言,应用程序打开这 ...

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

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

  3. 如何正确理解关键字"with"与上下文管理器

    转自:https://foofish.net/with-and-context-manager.html 如果你有阅读源码的习惯,可能会看到一些优秀的代码经常出现带有 “with” 关键字的语句,它通 ...

  4. (转)Python中的上下文管理器和Tornado对其的巧妙应用

    原文:https://www.binss.me/blog/the-context-manager-of-python-and-the-applications-in-tornado/ 上下文是什么? ...

  5. (转)contextlib — 上下文管理器工具

    原文:https://pythoncaff.com/docs/pymotw/contextlib-context-manager-tool/95 这是一篇社区协同翻译的文章,你可以点击右边区块信息里的 ...

  6. Python高级笔记(八)with、上下文管理器

    1. 上下文管理器 __enter__()方法返回资源对象,__exit__()方法处理一些清除资源 如:系统资源:文件.数据库链接.Socket等这些资源执行完业务逻辑之后,必须要关闭资源 #!/u ...

  7. asyncio之异步上下文管理器

    异步上下文管理器 前面文章我们提到了上下文管理器,但是这个上下文管理器只适用于同步代码,不能用于异步代码(async def形式),不过不用担心今天我们就来讨论在异步中如何使用上下文管理器. 特别提醒 ...

  8. Python 上下文管理器模块--contextlib

    在 Python 处理文件的时候我们使用 with 关键词来进行文件的资源关闭,但是并不是只有文件操作才能使用 with 语句.今天就让我们一起学习 Python 中的上下文管理 contextlib ...

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

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

随机推荐

  1. spring boot redis 写入异常

    redis 的 key value 使用 json 序列化.反序列化时,写入的 bean 不能是 final 类型的类,否则无法解析

  2. Centos7部署Redis集群

    Redis简介 Redis(Remote Dictionary Server)是完全开源的.遵守BSD协议的.高性能的Key-Value数据库. Redis与其他Key-Value缓存产品有一下三个特 ...

  3. 137_Power BI 自定义矩阵复刻Beyondsoft Calendar

    博客:www.jiaopengzi.com 焦棚子的文章目录 请点击下载附件 一.背景 前两天我们用PBI原生的视觉制作了自定义的热力图,今天我们来复刻一个Beyondsoft Calendar 1. ...

  4. 2020级cpp机考模拟题A卷-#题解1

    为了各位朋友的身心健康(不是),我们按照题目难度顺序来写题解. 第一次写题解,希望多点包容和鼓励(恬不知耻 1:谁先输出-4 题意: 输入3个整数,按从大到小的顺序输出,每两个数字间加一个空格. 题解 ...

  5. IDEA初始化基础配置

    0.前言 这篇博客是给认识的那帮新手搞的,刚进入IT行业的崽们 这个东西配置好了,也可以选择弄成在线文档,下一次安装IDEA时,有一个import导入配置,然后就可以自己配置好了( 虽然方便,但不建议 ...

  6. autohotkey(AHK)实现箭头映射

    起因 在主力本上使用了AHK实现alt ijkl的箭头映射,在另一个本子上怎么都不习惯,于是网上找教程,找了半天... 因为大家习惯了快捷键都不一样,为了避免以后浪费时间,因此开此文记录. 操作 ht ...

  7. 零成本搭建个人博客之图床和cdn加速

    本文属于零成本搭建个人博客指南系列 为什么要使用图床 博客文章中的图片资源文件一般采用本地相对/绝对路径引用,或者使用图床通过外链进行引用展示.本地引用的弊端我认为在于: 图片和博客放在同一个代码托管 ...

  8. 钉钉登录二维码嵌套在vue页面中

    转自 https://www.csdn.net/tags/OtDacg3sMjQ2NTgtYmxvZwO0O0OO0O0O.html 钉钉登录二维码嵌套在vue页面中 2021-09-04 14:42 ...

  9. 使用 vim 快速对当前文件夹下的文件批量重命名

    前言 我们在使用 Linux 的时候,有很多种方法可以对文件进行重命名,例如 命令行 下的 mv 命令,或者是使用像 dolphin 这样的图形文件管理器.但是有时候需要对一个文件夹下的所有文件批量重 ...

  10. 《HALCON数字图像处理》第六章笔记

    目录 第六章 图像增强 图像增强的概念和分类 灰度变换 直方图处理 图像的平滑 图像的锐化 图像的彩色增强 我在Gitee上建了个仓库,会将学习书本的时候打的一些代码上传上去,笔记中所有代码都在仓库里 ...