self = None是什么意思?

29

我正在阅读即将发布的asyncio包的源代码。请注意,在该方法的末尾,有一条self = None语句。它是做什么用的?

def _run(self):
    try:
        self._callback(*self._args)
    except Exception as exc:
        msg = 'Exception in callback {}{!r}'.format(self._callback,
                                                    self._args)
        self._loop.call_exception_handler({
            'message': msg,
            'exception': exc,
            'handle': self,
        })
    self = None  # Needed to break cycles when an exception occurs.

我以为它会删除该实例,但是下面的测试并没有表明如此:

class K:
    def haha(self):
        self = None

a = K()
a.haha()
print(a) # a is still an instance

6
也许问题应该是“为什么将self设置为None会破坏循环引用?是哪些循环引用?” - satoru
2个回答

28

这个函数简单的清除了对 self 的本地引用,确保如果发生异常,则传递给self._loop.call_exception_handler()的引用是唯一剩下的引用,并且没有创建循环。

这里仍然需要这样做,因为异常回溯引用了本地命名空间;当函数退出时,由于本地变量仍然存在引用,因此它将不会被清除

sys.exc_info()函数文档中记录了这一点并带有警告:

警告:在处理异常的函数中将traceback返回值分配给局部变量将导致循环引用。 这将防止同一函数或回溯所引用的任何局部变量被垃圾回收。 由于大多数函数不需要访问回溯,因此最佳解决方案是使用类似exctype,value = sys.exc_info()[:2]的东西来仅提取异常类型和值。 如果确实需要回溯,请确保在使用后删除它(最好使用try ... finally语句)或在不处理异常本身的函数中调用exc_info()

由于tulip处理程序形成了一个基本的框架类,因此该代码通过从本地命名空间中删除self来处理回溯循环引用情况,因为它无法保证_callbackcall_exception_handler函数将清除它们的引用。

在CPython中,对象在其引用计数降至0时被销毁,但是循环引用(一系列对象相互引用)永远不会将其引用计数降至0。垃圾收集器确实尝试打破这样的循环,但它不能总是做到这一点或不够快。显式清除引用可避免创建循环。

例如,如果存在一个__del__方法,垃圾回收器将不会打破循环,因为在这种情况下,它不知道以什么顺序安全地打破循环。即使没有__del__方法(框架类不应假设不会有这种情况),最好也不要依赖于垃圾收集器最终清除循环。

1
你能举个循环引用的例子吗? - msvalkon
我还是不明白...我的意思是,如果执行语句self = None,那么显然方法的结尾将被执行,在这种情况下,本地引用将无论如何退出作用域,那么为什么我们需要self = None - Derek Chiang
@DerekChiang:不会,因为当异常发生时有现场回溯,会维持本地命名空间的存在。 - Martijn Pieters
1
@msvalkon:任何直接或间接引用对象的内容都可以。self.ref = self 是最简单的例子。 - Martijn Pieters
1
@MartijnPieters,我不明白,即使我们从locals()中删除对self的引用,仍然存在一个循环,即self->_loop->self,难道不是吗? - satoru
6
或许是这样,但至少 self->call_exception_handler->exc->__traceback__->f_locals->self 这个循环已经消失了。 - Martijn Pieters

1
注意,这行文字是由Guido在修订版496中引入的。
在此修订版中,对应于_run的函数是run
def run(self):
    try:
        self._callback(*self._args)
    except Exception:
        tulip_log.exception('Exception in callback %s %r',
                            self._callback, self._args)
    self = None  # Needed to break cycles when an exception occurs.

tulip_log是一个普通的记录器:logging.getLogger("tulip")

在底层,Logger.exceptionsys.exc_info()的结果存储在LogRecord中,但是记录对象在exception调用后不会持久存在。

为了验证logging.exception不会导致引用循环,我进行了以下实验:

import time

import logging

class T:
    def __del__(self):
        print('T.__del__ called')

    def test(self):
        try:
            1 / 0
        except Exception:
            logging.exception("Testing")


def run():
    t = T()
    t.test()
    # t is supposed to be garbaged collected


run()

time.sleep(10) # to emulate a long running process

这是结果:
$ python test.py 
ERROR:root:Testing
Traceback (most recent call last):
  File "test.py", line 11, in test
    1 / 0
ZeroDivisionError: integer division or modulo by zero
T.__del__ called

对象t已按预期进行垃圾回收。

因此,我认为在这里不需要进行self = None赋值。


同一个提交在多个位置添加了 self = None,因为对于一个框架来说这是一个良好的实践,而不是因为代码库独立存在循环引用。 - Martijn Pieters
@MartijnPieters,您能否向我展示一下生成循环引用的示例?也许使用_callback - satoru
我还没有研究过 tulip / asyncio 库; 我不知道设计意图以及你所研究的提交是否是工作进行中(例如,即使在那时也不是完整的库)。logging库允许您注册自定义处理程序和格式化程序;不要仅仅依赖于默认的代码库。不幸的是,我现在没有时间构建一个样例案例(这个评论是通过 3G 连接的智能手机提供的)。 - Martijn Pieters

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