当前位置: 首页 > 后端技术 > Python

承前启后,Python3上下文管理器(ContextManagers)与With关键字的神话

时间:2023-03-25 22:30:03 Python

原文转载自《刘越的技术博客》https://v3u.cn/a_id_217在开发过程中,我们会经常面临一个常见的问题,就是如何正确管理外部资源,例如数据库、锁或网络连接。稍加小心,程序将永远保留这些资源,即使我们不再需要它们。这种类型的问题称为内存泄漏,因为每次在不关闭现有资源的情况下创建和打开给定资源的新实例时,可用内存都会减少。正确管理资源通常是一个棘手的问题,因为它们的使用通常需要善后工作。后果需要一些清理操作,例如关闭数据库、释放锁或关闭网络连接。如果您忘记执行这些清理操作,您可能会浪费宝贵的系统资源,例如内存和网络带宽。背景例如,当开发人员使用数据库时,可能出现的一个常见问题是程序不断创建新连接而不释放或重用它们。在这种情况下,数据库后端可以停止接受新连接。这可能需要管理员登录并手动终止这些过时的连接以使数据库再次可用。以著名的ORM工具Peewee为例:pip3installpymysqlpip3installpeewee在我们声明了数据库实例后,我们尝试链接数据库:frompeeweeimportMySQLDatabasedb=MySQLDatabase('mytest',user='root',password='root',host='localhost',port=3306)print(db.connect())程序输出:True但如果重复创建链接:frompeeweeimportMySQLDatabasedb=MySQLDatabase('mytest',user='root',password='root',host='localhost',port=3306)print(db.connect())print(db.connect())会抛出异常:Traceback(mostrecentcalllast):File"/Users/liuyue/Downloads/upload/test/test.py”,第23行,在print(db.connect())文件“/opt/homebrew/lib/python3.9/site-packages/peewee.py",第3129行,在连接中引发OperationalError('Connectionalreadyopened.')peewee.OperationalError:Connectionalreadyopened。因此需要手动关闭数据库连接:frompeeweeimportMySQLDatabasedb=MySQLDatabase('mytest',user='root',password='root',host='localhost',port=3306)print(db.connect())打印(db.close())print(db.connect())返回:TrueTrueTrue,但此操作存在潜在问题。如果connect调用过程中出现异常,导致后续代码无法继续执行,close方法就无法正常调用,所以数据库资源会一直被程序占用,无法释放继续改进:frompeeweeimportMySQLDatabasedb=MySQLDatabase('mytest',user='root',password='root',host='localhost',port=3306)try:print(db.connect())除了OperationalError:print("Connectionalreadyopened.")finally:print(db.close())改进的逻辑是在可能发生异常的代码处捕获OperationalError异常,并使用try/finally语句,这个语句的意思是如果程序在try代码块中发生了异常,后面的代码不会执行,直接跳转到except代码块。最后,执行finally块逻辑的代码。因此,只要将close方法放在finally块中,就会关闭数据库连接。其实Peewee为我们提供了一种更简洁优雅的数据库链接操作方式:frompeeweeimportMySQLDatabasedb=MySQLDatabase('mytest',user='root',password='root',host='localhost',port=3306)withdb.connection_context():print("dbisopen")print(db.is_closed())是使用with关键字进行操作,这里使用with打开数据库的contextmanager,当程序运行时离开with关键字的范围,系统会自动调用close方法。最终效果与上面捕获OperationalError的异常一致,系统会自动关闭数据库连接。上下文管理器(ContextManagers)那么Peewee底层是如何实现数据库的自动关闭的呢?那就是使用Python3内置的contextmanager。在Python中,任何实现了\_\_enter\_\_()和\_\_exit\_\_()方法的对象都可以称为上下文管理器。上下文管理器对象可以使用with关键字:frompeeweeimportMySQLDatabasedb=MySQLDatabase('mytest',user='root',password='root',host='localhost',port=3306)classDb:def__init__(自我):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类对象:withDb()asdb:print("dbisopening")print(Db().db.is_closed())程序返回:dbis开启True,我们不需要显式调用close方法,系统会自动调用,即使中途抛出异常,close方法理论上也会被调用。上下文语法糖Python3还提供了基于上下文管理器的装饰器,进一步简化了上下文管理器的实现。该方法被生成器yield关键字分为两部分,yield之前的语句在\_\_enter\_\_方法中执行,yield之后的语句在\_\_exit\_\_方法中执行。yield后面的值是函数的返回值:frompeeweeimportMySQLDatabasefromcontextlibimportcontextmanager@contextmanagerdefmydb():db=MySQLDatabase('mytest',user='root',password='root',host='localhost',port=3306)yielddbdb.close()然后通过with关键字调用contextmanager修饰的方法:withmydb()asdb:print("dbisopening")同时,Peewee还贴心的帮我们封装了这个装饰器:frompeeweeimportMySQLDatabasedb=MySQLDatabase('mytest',user='root',password='root',host='localhost',port=3306)@db.connection_context()defmydb():print("dbisopen")mydb()看起来没问题。误解:上下文管理是否一定能解决善后问题?请不要太确定,是的,上下文管理器很漂亮,但并不完美。在某些极端情况下,它可能无法处理善后事宜:frompeeweeimportMySQLDatabaseclassDb:def__init__(self):self.db=MySQLDatabase('mytest',user='root',password='root',host='localhost',port=3306)def__enter__(self):print("connect")self.db.connect()退出(-1)def__exit__(self,*args):print("close")self.db.close()withDb()asdb:print("dbisopening")程序返回:connect当我们通过with关键字调用它时当上下文管理器在\_\_enter\_\_方法,通过exit()方法强制关闭程序,过程中程序会立即结束,不会进入\_\_exit\_\_方法执行关闭过程。换句话说,数据库连接没有正确关闭。同理,当我们写finally关键字的时候,理论上当然会执行finally代码块,但实际上只是理论上的:defgen(text):try:forlineintext:try:yieldint(行)除了:最后通过:print('Aftermathwork')text=['1','','2','','3']ifany(n>1forningen(text)):print('Foundanumber')print('Notdealwiththeaftermath')程序返回:Exceptionignoredin:Traceback(mostrecentcalllast):File"/Users/liuyue/Downloads/upload/test/test.py",line71,inifany(n>1forningen(text)):RuntimeError:generatorignoredGeneratorExitFoundanumber后果不明显。当程序进入finally代码块时,会立即触发生成器异常。当理论上捕获到异常时,程序通过yield返回到原来的状态,所以立即退出,放弃finally逻辑的执行。所以,从逻辑上讲,我们不能指望上下文管理器每次都能帮我们“收拾残局”。至少,当事情还没有结束时,我们可以适应这种情况:frompeeweeimportMySQLDatabaseclassDb:def__init__(self):self.db=MySQLDatabase('mytest',user='root',password='root',host='localhost',port=3306)def__enter__(self):ifself.db.is_closed():print("connect")self.db.connect()def__exit__(self,*args):print("close")self.db.close()withDb()asdb:print("dbisopening")print(Db().db.is_closed())结论使用With关键字操作上下文管理器可以管理外部资源更快捷,同时提高了代码的健壮性和可读性,但在极端情况下,上下文管理器并不是万能的。仍然需要诸如轮询服务之类的支持保护方案。原文转载自《刘越的技术博客》https://v3u.cn/a_id_217