Lifetime of static class members/class references?

2019-09-09 21:43发布

I was asked to show how to do a singleton like solution for the old chestnut of a special logger. At pains to point out the reasons for not doing this sort of thing, still, I tried.

In doing so, I have a static class member disappearing unexpectedly.

With this class declaration:

epiLogger.py:

import logging

class epiLogger():
    _initialised = {}
    _finalised = {}

    def __init__(self, name):
        self.logger = logging.getLogger(name)
        self.name = name
        if not epiLogger._initialised.get(name):
            self.logger.addHandler(logging.StreamHandler())
            self.logger.setLevel(logging.INFO)
            self.logger.info('** My Prologue **')
            epiLogger._initialised[self.name] = True

    def info(self, the_info):
        self.logger.info(the_info)
        print epiLogger._finalised.get(self.name)

    def __del__(self):
        print "destructing", self.name
        if not epiLogger._finalised.get(self.name):
            print "first destruction"
            self.logger.info('** My Epilogue **')
            epiLogger._finalised[self.name] = True

And these test files:

bar.py:

from epiLogger import *

a = epiLogger("bar")

a.info("foo!")
a.info("bar!")
a.info("party!")

test.py:

import bar

I get

~ mgregory$ python test.py
** My Prologue **
foo!
None
bar!
None
party!
None
destructing bar
Exception AttributeError: "'NoneType' object has no attribute '_finalised'" in <bound method epiLogger.__del__ of <epiLogger.epiLogger instance at 0x1004a48c0>> ignored
~ mgregory$  

But if I run just the bar.py file:

~ mgregory$ python bar.py
** My Prologue **
foo!
None
bar!
None
party!
None
destructing bar
first destruction
** My Epilogue **
~ mgregory$ 

It seems that one level of indirection has resulted in the reference to the class itself (to access the class variable) has become "None".

I tried a simpler test case, and it does not fail in this way (!)

frob.py:

class frob():    
    _nasty_global_thingy = True

    def __init__(self):
        print "initialising a foo", frob._nasty_global_thingy

    def __del__(self):
        print "destroying a foo", frob._nasty_global_thingy

bar.py:

from frob import *

a = frob()
print a

This does not fail in the same way upon import of bar.py.

I understand that this is one of many reasons not to try this sort of thing, but I would like to understand what is going on, nonetheless.

1条回答
男人必须洒脱
2楼-- · 2019-09-09 22:16

Module globals are cleaned up on Python exit, and your class reference is already gone by the time the __del__ hook is run.

Don't count on globals still being there. Rather, use type(self) to get the class reference:

def __del__(self):
    print "destructing", self.name
    cls = type(self)
    if not cls._finalised.get(self.name):
        print "first destruction"
        self.logger.info('** My Epilogue **')
        cls._finalised[self.name] = True

This is documented in the big Warning section on the object.__del__ hook documentation:

Also, when __del__() is invoked in response to a module being deleted (e.g., when execution of the program is done), other globals referenced by the __del__() method may already have been deleted or in the process of being torn down (e.g. the import machinery shutting down). For this reason, __del__() methods should do the absolute minimum needed to maintain external invariants.

Take into account that module globals are maintained in a dictionary, and thus the order in which they are cleared is subject to the current implementation-and-Python-version-dependent order of the dictionary when being cleared.

查看更多
登录 后发表回答