What does self = None do?

Question:

I’m reading the source code of the incoming asyncio package. Note that at the end of the method, there is a self = None statement. What does it do?

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.

I thought it would erase the instance, but the following test doesn’t suggest so:

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

a = K()
a.haha()
print(a) # a is still an instance
Asked By: Derek Chiang

||

Answers:

It simply clears the local reference to self, making sure that if an exception occurs the reference passed to self._loop.call_exception_handler() is the only remaining reference and no cycle has been created.

This is still needed here because the local namespace is referenced by the exception traceback; it will not be cleared up when the function exits as there is a reference to the locals alive still.

This is documented in the sys.exc_info() function documentation with a warning:

Warning: Assigning the traceback return value to a local variable in a function that is handling an exception will cause a circular reference. This will prevent anything referenced by a local variable in the same function or by the traceback from being garbage collected. Since most functions don’t need access to the traceback, the best solution is to use something like exctype, value = sys.exc_info()[:2] to extract only the exception type and value. If you do need the traceback, make sure to delete it after use (best done with a try ... finally statement) or to call exc_info() in a function that does not itself handle an exception.

Because tulip handlers form a fundamental framework class the code handles the traceback circular reference case by removing self from the local namespace instead, as it cannot guarantee that the _callback or call_exception_handler functions will clear up their references.

In CPython, objects are destroyed when their reference count drops to 0, but a cyclic reference (a series of objects referencing themselves in a cycle) will never see their reference count drop to 0. The garbage collector does try to break such cycles but it cannot always do this or not fast enough. Explicitly clearing references avoids creating cycles.

For example, if there is a __del__ method, the garbage collector will not break a cycle as it won’t know in what order to break a cycle safely in that case.

Even if there is no __del__ method (which a framework class should never assume will not be the case) it’s best to not rely on the garbage collector eventually clearing cycles.

Answered By: Martijn Pieters

Note that this line is introduced in revision 496 by Guido.

At this revision, the function that corresponded to _run is 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 is just a normal logger: logging.getLogger("tulip").

Under the hood, Logger.exception stores the result of sys.exc_info() in LogRecord, but the record object doesn’t persist after the exception call.

To verify that logging.exception doesn’t cause reference cycle, I did the following experiment:

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

This is the result:

$ 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

The object t is garbage collected as expected.

So, I don’t think that self = None assignment is necessary here.

Answered By: satoru