为啥 MySQLdb 连接上下文管理器不关闭游标?
Posted
技术标签:
【中文标题】为啥 MySQLdb 连接上下文管理器不关闭游标?【英文标题】:Why doesn't the MySQLdb Connection context manager close the cursor?为什么 MySQLdb 连接上下文管理器不关闭游标? 【发布时间】:2015-10-01 05:09:39 【问题描述】:mysqldb Connections
有一个基本的上下文管理器,它在 enter 时创建一个游标,在 exit 时回滚或提交,并且不会隐式抑制异常。来自Connection source:
def __enter__(self):
if self.get_autocommit():
self.query("BEGIN")
return self.cursor()
def __exit__(self, exc, value, tb):
if exc:
self.rollback()
else:
self.commit()
那么,有谁知道为什么退出时光标没有关闭?
起初,我认为这是因为关闭游标没有做任何事情,并且游标只有一个关闭方法以尊重Python DB API(参见this answer 的cmets)。然而,事实是关闭游标会烧掉剩余的结果集(如果有的话),并禁用游标。来自cursor source:
def close(self):
"""Close the cursor. No further queries will be possible."""
if not self.connection: return
while self.nextset(): pass
self.connection = None
在退出时关闭光标会很容易,所以我不得不假设它不是故意的。另一方面,我们可以看到当一个游标被删除时,它无论如何都会关闭,所以我猜垃圾收集器最终会解决它。我对 Python 中的垃圾回收了解不多。
def __del__(self):
self.close()
self.errorhandler = None
self._result = None
另一种猜测是,可能存在您想在with
块之后重用光标的情况。但我想不出你需要这样做的任何理由。难道你不能总是在其上下文中使用完游标,而只为下一个事务使用单独的上下文吗?
说得很清楚,这个例子显然没有意义:
with conn as cursor:
cursor.execute(select_stmt)
rows = cursor.fetchall()
应该是:
with conn as cursor:
cursor.execute(select_stmt)
rows = cursor.fetchall()
这个例子也没有意义:
# first transaction
with conn as cursor:
cursor.execute(update_stmt_1)
# second transaction, reusing cursor
try:
cursor.execute(update_stmt_2)
except:
conn.rollback()
else:
conn.commit()
应该是:
# first transaction
with conn as cursor:
cursor.execute(update_stmt_1)
# second transaction, new cursor
with conn as cursor:
cursor.execute(update_stmt_2)
再说一遍,退出时关闭光标有什么害处,不关闭有什么好处?
【问题讨论】:
一个很好的解释>>> ***.com/questions/5669878/… @syed 这是我的一个老问题,我也在这个问题中链接到它! 糟糕!没注意到。我想这个解释仍然成立。对此没有具体的答案,除此之外,您需要自行确定。 值得指出的是,MySQLdb
源对底层_mysql
模块(与MySQLdb
作为.pyd
二进制文件一起分发)或the API it implements 提供的信息相对较少。我倾向于假设包作者遇到了以这种方式做事的理由,埋在其中一个依赖项中,或者它以 YAGNI 开始并以“没有损坏”而结束。
【参考方案1】:
直接回答您的问题:我看不出在 with
块的末尾关闭有任何危害。我不能说为什么在这种情况下不这样做。但是,由于在这个问题上缺乏活动,我搜索了代码历史,并会提出一些想法(猜测)为什么@987654329 @可能不会被调用:
通过对nextset()
的调用可能引发异常的可能性很小——这可能已被观察到并被视为不可取。这可能是newer version of cursors.py
在close()
中包含此结构的原因:
def close(self):
"""Close the cursor. No further queries will be possible."""
if not self.connection:
return
self._flush()
try:
while self.nextset():
pass
except:
pass
self.connection = None
有可能(有点遥远)可能需要一些时间才能完成所有剩余的结果,什么都不做。因此close()
可能不会被调用以避免进行一些不必要的迭代。我认为,您是否认为值得保存这些时钟周期是主观的,但您可以按照“如果没有必要,就不要这样做”的思路来争论。
浏览 sourceforge 提交,该功能于 2007 年由 this commit 添加到主干中,看来 connections.py
的这一部分从那时起就没有改变。这是基于this commit 的合并,其中包含消息
为http://docs.python.org/whatsnew/pep-343.html 中所述的 with 语句添加 Python-2.5 支持请测试
你引用的代码从那以后就再也没有改变过。
这提示了我最后的想法 - 这可能只是第一次尝试/原型刚刚工作,因此从未改变。
更现代的版本
您链接到旧版本连接器的源。我注意到同一库 here 有一个更活跃的分支,我在我的 cmets 中链接到第 1 点中关于“较新版本”的内容。
请注意,此模块的更新版本已在 cursor
本身内实现 __enter__()
和 __exit__()
:see here。 __exit__()
这里 确实 call self.close()
或许这提供了一种更标准的方式来使用 with 语法,例如
with conn.cursor() as c:
#Do your thing with the cursor
尾注
NB 我想我应该补充一下,据我了解垃圾收集(也不是专家),一旦没有对 conn
的引用,它将是解除分配。此时将没有对游标对象的引用,它也将被释放。
然而调用cursor.close()
并不意味着它会被垃圾回收。它只是烧掉结果并将连接设置为None
。这意味着它不能被重复使用,但不会立即被垃圾收集。您可以通过在 with
块之后手动调用 cursor.close()
来说服自己,然后,例如,打印 cursor
的某些属性
注意2 我认为with
语法的这种用法有点不寻常,因为conn
对象仍然存在,因为它已经在外部范围内——不像更常见的with open('filename') as f:
,其中在with
块结束后,没有任何带有引用的对象。
【讨论】:
是的,with conn.cursor() as c
确实更有意义,我很高兴它实际上已在该版本中关闭。我可能会尝试切换到该版本。在这一点上这是一个没有实际意义的问题,但我关于垃圾收集的注意事项是无论如何都会在收集期间调用关闭,所以为什么要打扰(而不是是否以及何时收集它)。如果我不得不猜测,您的#3 似乎是最有可能的解释。
好的 - 是的 - 我明白你关于在垃圾收集期间调用 close
的观点:你是对的,我误解了你的观点。不过,感谢您的赏金 - 抱歉,我无法给出明确的结论,但我同意 #3 可能是最有可能的。
我意识到这是一个古老的问题和答案,但我认为值得注意的是问题中显示的 __exit__
方法在 Connection
对象上调用,而不是在 Cursor
对象上调用从__enter__
方法返回。这与 Python 的文件对象不同,后者从 __enter__
返回 self
。因为Connection.__enter__
没有保存对它返回的Cursor
的引用(至少,不是直接作为__enter__
的一部分),所以__exit__
没有明显的方法可以关闭它。做self.close()
可能不合适!以上是关于为啥 MySQLdb 连接上下文管理器不关闭游标?的主要内容,如果未能解决你的问题,请参考以下文章
为啥 django 和 python MySQLdb 每个数据库有一个游标?
pycharm编写python,图中import MySQLdb没问题,import mysql.connector出错,为啥会出错?