为啥 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.pyclose() 中包含此结构的原因:

    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 连接上下文管理器不关闭游标?的主要内容,如果未能解决你的问题,请参考以下文章

MySQLdb

为啥 django 和 python MySQLdb 每个数据库有一个游标?

python mysqldb 一个连接的多个游标

为啥需要重新连接数据库才能看到表数据的变化?

MySQLdb 最佳实践 [关闭]

pycharm编写python,图中import MySQLdb没问题,import mysql.connector出错,为啥会出错?