Borg - is that a singleton?

Alex Martelli aleax at aleax.it
Sat Mar 1 09:20:27 CET 2003


Giovanni Bajo wrote:
   ...
> Since Borg (as in
> http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66531) rebinds the

Expanded and clarified in the printed Cookbook (O'Reilly) and in my
IPC11 essay, online at www.aleax.it/Python/5ep.html -- qv.

> class Borg:
>     def __init__(self):
>         cls = self.__class__
>         try: cls.__shared_state
>         except:
>             cls.__shared_state = {}
>         self.__dict__ = cls.__shared_state
> 
> Which leads to what seems to me the expected behaviour from a singleton,

I suggest, as explained in the paper:

class Borg:
    _shared_state = {}          # only ONE leading _
    def __init__(self):
        self.__dict__ = self._shared_state

class Monitor(Borg):
    _shared_state = {}          # all Monitors share state among themselves

class SpeclalMonitor(Monitor):
    pass                        # including SpecialMonitor instances

class Printer(Borg):
    _shared_state = {}          # all Printers share state among themselves

class SpeclalPrinter(Printer):
    pass                        # including SpecialPrinter instances


If you want to FORCE each direct subclass of Borg to define its own
_shared_state, just omit the assignment to _shared_state in the body
of the Borg class itself.  But subclasses of such direct classes
should still be _allowed_ to keep sharing state with their base, in
order to customize its behavior but not the state, if desired.


I find your suggested approach a bit fragile, because it conflates
issues of class initialization with ones of instance initialization.
Consider, with your implementation of Borg:

class Super(Borg): pass
class Sub(Super): pass

now if the code continues with:
  a = Sub()
  b = Super()
then a and b do NOT share state.  But if it instead continues with:
  b = Super()
  a = Sub()
then a and b DO share state.  This is far from obvious to the reader
of the code, and it's what I would call a typical "black magic effect".

If you DO want to avoid the clear, simple, explicit solution of
having classes that don't want their instances to share state with
instances of superclasses just bind their own _shared_state dict
in class scope, you're probably better off generating the magic
_shared_state in a custom metaclass, e.g.:


class metaBorg(type):

    def __new__(cls, name, bases, clasdict):
        clasdict.setdefault('_shared_state', {})
        return type.__new__(cls, name, bases, clasdict)

    def __call__(cls, *args, **kwds):
        self = cls.__new__(cls, *args, **kwds)
        self.__dict__ = self._shared_state
        cls.__init__(self, *args, **kwds)
        return self


class Borg:
    __metaclass__ = metaBorg


This version _forces_ a _shared_state class attribute on each
subclass (though it also lets such a subclass explicitly define
a _shared_state for nefarious purposes and doesn't override it
in that case -- that's why the setdefault call).  You might
chose to soften this a bit, e.g. by making metaBorg.__new__
into:

    def __new__(cls, name, bases, clasdict):
        for base in bases: 
            if hasattr(base, '_shared_state'): break
        else:
            clasdict.setdefault('_shared_state', {})
        return type.__new__(cls, name, bases, clasdict)

this LETS each subclass define a _shared_state, but only
defaults to setting it to a new empty dict if no base has
a _shared_state attribute already.

I thought this metaclass might be of some interest because
it also overrides type.__call__ (a bit roughly perhaps -- it
does not allow classes to set a weird __new__, but rather
assumes __new__ returns a cls instance and unconditionally
calls __init__ on that instance) in order to automate the
setting of each instance's __dict__, without any need for
borgs to explicitly call Borg.__init__(self) any more.


Whether this level of "implicitness" qualifies as black
magic best avoided, or as an elegant solution, I'll leave
to readers to decide.  Anyway, the point is you can now:

class Foo(Borg): pass
class Bar(Borg): pass

and get _separately-borgized_ Foo and Bar classes without
doing anything more than inheriting from Borg.


Alex





More information about the Python-list mailing list