乐趣区

关于数据库:承上启下继往开来Python3上下文管理器ContextManagers与With关键字的迷思

原文转载自「刘悦的技术博客」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

退出移动版