Search code examples
pythondesign-patternspython-3.xfinalizer

Python - Observer pattern - Object has no attribute


I try to run one example from the book "Python Essential Reference" involving observer pattern, but there is a problem with attribute. When the AccountObserver executes __del__ the error raises - Object has no attribute 'observers'. I don't know what wrong is with the code, so any help would be very appreciate.

class Account(object):
    def __init__(self, name, balance):
        self.name = name
        self.balance = balance
        self.observers = set()
    def __del__(self):
        for ob in self.observers:
            ob.close()
        del self.observers
    def register(self, observer):
        self.observers.add(observer)
    def unregister(self, observer):
        self.observers.remove(observer)  
    def notify(self):
        for ob in self.observers:
            ob.update()     
    def withdraw(self, amt):
        self.balance -= amt
        self.notify()


class AccountObserver(object):
    def __init__(self, theaccount):
        self.theaccount = theaccount
        self.theaccount.register(self)

    def __del__(self):
        self.theaccount.unregister(self)
        del self.theaccount

    def update(self):
        print("Balance is %0.2f" % self.theaccount.balance)

    def close(self):
        print("Account no longer in use")


a = Account("Ketty", 200000)
a_mama = AccountObserver(a)
a_tata = AccountObserver(a)
a.unregister(a_mama)
a.withdraw(10)

And the output:

Balance is 199990.00
Account no longer in use
Exception ignored in: <bound method AccountObserver.__del__ of <__main__.AccountObserver object at 0x024BF9F0>>
Traceback (most recent call last):
  File "F:\Projects\TestP\src\main.py", line 28, in __del__
  File "F:\Projects\TestP\src\main.py", line 13, in unregister
AttributeError: 'Account' object has no attribute 'observers'
Exception ignored in: <bound method AccountObserver.__del__ of <__main__.AccountObserver object at 0x024BFEB0>>
Traceback (most recent call last):
  File "F:\Projects\TestP\src\main.py", line 28, in __del__
  File "F:\Projects\TestP\src\main.py", line 13, in unregister
AttributeError: 'Account' object has no attribute 'observers'

Solution

  • Python cleans out the module when the interpreter exits. At that point all instances and classes are deleted, and that means that Account.__del__ can run before AccountObserver.__del__. The order in which the classes are cleared depends on the global namespace dictionary order, which is random thanks to the random hash seed used. Account.__del__ deletes self.observers so any later call to account.unregister() will raise an AttributeError.

    Your code relies on the classes and attributes all still being there when the module exits. That means you can get both KeyError errors (as a_mama was already unregistered), or AttributeError as the self.observers attribute is already cleared (because Account.__del__ cleared it).

    There is a big fat warning in the object.__del__ documentation:

    Warning: Due to the precarious circumstances under which __del__() methods are invoked, exceptions that occur during their execution are ignored, and a warning is printed to sys.stderr instead. 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. Starting with version 1.5, Python guarantees that globals whose name begins with a single underscore are deleted from their module before other globals are deleted; if no other references to such globals exist, this may help in assuring that imported modules are still available at the time when the __del__() method is called.

    The work-around is to make your __del__ method more robust in the face of such exceptions:

    def unregister(self, observer):
        try:
            self.observers.remove(observer)  
        except (KeyError, AttributeError):
            # no such observer, or the observers set has already been cleared