0

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.

Community
  • 1
  • 1
GreenAsJade
  • 14,459
  • 11
  • 63
  • 98

1 Answers1

2

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.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • @GreenAsJade: Depending on your exact Python version that could just be the roll of the dice as hash randomisation may have altered the order in which globals are cleaned up. – Martijn Pieters Sep 30 '14 at 11:07
  • Heh - your addition of documentation helped a lot, thanks! It made me realise, if nothing else, that I need to think about "what does it mean that a _module_ is being deleted!?" (I've previously only thought about object deletion!) – GreenAsJade Sep 30 '14 at 11:09
  • 1
    See also [Can a simple difference in Python3 variable names alter the way code runs?](http://stackoverflow.com/q/22864764) – Martijn Pieters Sep 30 '14 at 11:10
  • (crappy title of the original question - I'd never have found that, while looking for the answer for this! :) ) – GreenAsJade Sep 30 '14 at 11:13
  • So... I implmented as above, but get Exception AttributeError: "type object 'instance' has no attribute '_finalised'" in > ignored in all cases. Thoughts? – GreenAsJade Sep 30 '14 at 11:18
  • 1
    @GreenAsJade: ah, you have a old-style Python 2 class. Use `self.__class__` instead of `type(self)`. That or make the class inherit from `object`. – Martijn Pieters Sep 30 '14 at 11:19