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
def __init__(self, n) -> None: means that __init__ should always return NoneType and it can be quite helpful if you accidentally return something different from None especially if you use mypy or other similar things. But you can ignore it if you prefer the old way to do it. Follow this answer to receive notifications.
The None keyword is used to define a null value, or no value at all. None is not the same as 0, False, or an empty string.
It is a type annotation for the main function that simply states that this function returns None . Type annotations were introduced in Python 3.5 and are specified in PEP 484 . Annotations for the return value of a function use the symbol -> followed by a type.
It's often used as the default value for optional parameters, as in: def sort(key=None): if key is not None: # do something with the argument else: # argument was omitted. If you only used if key: here, then an argument which evaluated to false would not be considered.
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 atry ... finally
statement) or to callexc_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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With