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

19

MySQLdb Connections具有简单的上下文管理器,当进入时创建一个游标,在退出时要么回滚要么提交,并且隐式地不会抑制异常。请参考连接源码

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的敬意(请参见此答案的评论)。然而,事实上关闭光标会耗尽剩余的结果集(如果有的话),并禁用光标。从光标源代码来看:

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)

再次强调,关闭游标会有什么危害?不关闭游标有哪些好处?


1
一个很好的解释 >>> https://dev59.com/b2035IYBdhLWcg3wBLRL - Syed Mauze Rehan
@syed 这是我的一个旧问题,我在这个问题中也链接了它! - jmilloy
哎呀!没注意到。我想那个解释仍然适用。除了你自己要确保之外,没有具体的答案。 - Syed Mauze Rehan
1
值得一提的是,MySQLdb源代码相对较少地涉及底层 _mysql 模块(作为 .pyd 二进制文件与 MySQLdb 一起分发),或者其实现的API。我倾向于认为,要么包的作者遇到了在这些依赖项中埋藏的原因,要么它开始时只是YAGNI,最终变成了“不破不立”。 - Air
1个回答

11
回答您的问题:我认为在with块结束时关闭连接不会有任何害处。我不能确定为什么在这种情况下没有这样做。但是,由于这个问题的活动很少,我查看了代码历史记录,并提供一些关于为什么可能不会调用close()的想法(猜测):
1.循环调用nextset()可能会抛出异常的可能性很小-可能已经观察到并被视为不希望发生的情况。这可能是为什么新版本的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年添加到主干中的,自那以后,在connections.py中的这个部分似乎没有改变过。这是基于此提交合并的,其中包含以下消息:

    添加Python-2.5支持with语句,如http://docs.python.org/whatsnew/pep-343.html所述 请测试

    并且你引用的代码从未改变过。

    这促使我想到最后一个思考——它可能只是第一次尝试/原型,因此从未被更改。


  • 更现代的版本

    你链接到了连接器的遗留版本源代码。我注意到有一个同样的库的更活跃的分支在这里,我在关于第一点“新版本”的评论中提供了链接。

    请注意,此模块的更新版本已在cursor本身内实现了__enter__()__exit__()见这里__exit__()在这里调用了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块结束后,没有对象挂起引用。


    是的,with conn.cursor() as c 更有意义,我很高兴在那个版本中它实际上被关闭了。我可能会尝试切换到那个版本。这个问题现在已经没有意义了,但我的注意事项是垃圾回收期间会调用关闭,所以为什么要费心呢(而不是是否和何时进行回收)。如果我必须猜测,你的#3似乎是最有可能的解释。 - jmilloy
    好的 - 是的 - 我明白你关于close在垃圾回收期间被调用的观点:你是对的,我误解了你所表达的观点。虽然我不能给出明确的答案,但还是感谢你的慷慨奖励,我同意#3可能是最有可能的。 - J Richard Snape
    我知道这是一个旧的问题和答案,但我认为值得注意的是,在问题中显示的__exit__方法被调用的是Connection对象,而不是从__enter__方法返回的Cursor对象。这与Python的文件对象不同,后者从__enter__返回self。因为Connection.__enter__没有保存对其返回的Cursor的引用(至少不是直接作为__enter__的一部分),所以__exit__无法明显地关闭它。执行self.close()可能不合适! - Blckknght

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接