为什么 MySQLdb 连接上下文管理器不关闭游标?

Why doesn't the MySQLdb Connection context manager close the cursor?

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()

所以,有谁知道为什么光标在退出时没有关闭?


起初,我认为这是因为关闭游标没有做任何事情,并且游标只有一个 close 方法以尊重 Python DB API (see the comments to this answer). However, the fact is that closing the cursor burns through the remaining results sets, if any, and disables the cursor. From the 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)

同样,退出时关闭游标有什么坏处,不关闭它有什么好处?

直接回答您的问题:我看不出在 with 块末尾关闭有任何危害。我不能说为什么在这种情况下不这样做。但是,由于在这个问题上缺乏 activity,我搜索了代码历史并提出了一些想法(guesses) 关于为什么 close() 可能 不被调用:

  1. 通过对 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
    
  2. 有可能(有点遥远)可能需要一些时间来浏览所有剩余的结果,什么都不做。因此可能不会调用 close() 以避免进行一些不必要的迭代。我想,您是否认为节省这些时钟周期是值得的是主观的,但您可以按照 "if it's not necessary, don't do it" 的思路争论。

  3. 浏览 sourceforge 提交,该功能已由 this commit in 2007 and it appears that this section of connections.py has not changed since. That's a merge based on this commit 添加到主干,其中包含消息

    Add Python-2.5 support for with statement as described in http://docs.python.org/whatsnew/pep-343.html Please test

    并且您引用的代码此后从未更改过。

    这引发了我的最终想法 - 这可能只是第一次尝试/原型刚刚起作用,因此从未改变过。


更现代的版本

您 link 寻找旧版连接器的来源。我注意到同一个库 here 有一个更活跃的分支,我 link 在我对第 1 点 "newer version" 的评论中提到了它。

请注意,此模块的较新版本已在 cursor 本身内实现了 __enter__()__exit__()see here. __exit__() here does call self.close(),也许这提供了更标准的使用方式with 语法例如

with conn.cursor() as c:
    #Do your thing with the cursor

尾注

N.B. 我想我应该补充一下,据我了解垃圾收集(也不是专家)一旦没有对 conn 的引用,它将被释放。此时将没有对游标对象的引用,它也将被释放。

但是调用cursor.close()并不意味着它会被垃圾回收。它只是简单地遍历结果并将连接设置为 None。这意味着它不能被重复使用,但不会立即被垃圾收集。您可以通过在 with 块之后手动调用 cursor.close() 然后打印 cursor

的某些属性来说服自己这一点

N.B。 2 我认为这是对 with 语法的一种不寻常的使用,因为 conn 对象仍然存在,因为它已经在外部范围内 - 不像,说,更常见的是 with open('filename') as f:,其中在 with 块结束后没有对象与引用挂在一起。