Package harold.prog.namespace
Unambiguous inheritance in Python.
Rationale
Though the mro
and super
mechanisms do a
great job in selecting the correct method from multiple superclasses,
there are still situations where ambiguity remains, and is resolved in
Python by relying on the order in which the superclasses are declared
in the class definition. Though simple and efficient in most cases,
there are cases where it is not satisfactory, namely when inheriting
homonym members from several superclasses.
Let us give two typical examples where such an ambiguity
happens.
^-shaped inheritance
This is what happens in the classical diamond case (A inherits B
and C which both inherit D) when classes B and C both override a
method from D. Here is a more illustrative example:
class Musician (object):
def play (self, instrument):
return "I'm playing the " + instrument
class HappyMusician (Musician):
def play (self, instrument):
return super (HappyMusician, self).play (instrument) \
+ " with a smile on my face"
class LousyMusician (Musician):
def play (self, instrument):
return super (LousyMusician, self).play (instrument) \
+ " lousily"
class HappyLousyMusician (HappyMusician, LousyMusician):
pass
HappyLousyMusician().play ("piano")
The play
method in the latter class is ambiguous.
This example is not too bad because the use of super
ensures that both HappyMusician.play
and
LousyMusician.play
will be called (the last line
resulting here in "I'm playing the piano lousily with a smile on
my face"). However, this is not always possible, because:
-
all methods are not adapted to be combined with
super
-
the result still depends on the order of the superclasses ;
for example, changing that order will result in "I'm playin
the piano with a smile on my face lousily" ; this might not
be satisfactory
It is possible, however, to override both methods in the subclass
to solve the ambiguity explicitly.
Parallel definitions
This scenario is more tricky. It happens when two superclasses
independantly define methods with the same name. Obviously, those
methods do not have the same semantics. For example:
class Musician (object):
def play (self, instrument):
return "I'm playing the " + instrument
def do_piano (self):
return self.play ("piano")
class Player (object):
def play (self, game):
return "I'm playing " + game
def do_poker (self):
return self.play ("poker")
class MusicianPlayer (Musician, Player):
pass
mp = MusicianPlayer ()
mp.play ("thing")
mp.do_piano ()
mp.do_yoyo ()
Obviously, both play
methods do not have the same
semantics, so it is not possible, as in the previous case, to
unify them by overriding in MusicianPlayer
.
Here, Musician.play
and Player.play
not
only have different implementations, they have non-overlapping
semantics. A call to mp.play
is here totally ambiguous,
because we have no mean to determine if this is to play music or to
play a game.
Furthermore, Python's resolution mechanism semantically breaks the
method do_poker
in MusicianPlayer
, which
will return "I'm playing the poker" rather than
"I'm playing poker". Of course, changing the order in the
superclasses would fix do_poker
but break
do_piano
instead.
There is no simple solution here. Indeed, mp.play
is
absolutely ambiguous, and since member lookup is always performed at
runtime in Python, it becomes ambiguous even in do_piano
and do_poker
, and as Tim Peters' Zen of Python states,
"In the face of ambiguity, refuse the temptation to
guess".
A solution: namespaces
Here is an implementation of MusicianPlayer which would solve our
problems:
class FixedMusicianPlayer (Musician,Player):
def play (self, *args, **kw):
raise Exception, "Ambiguous: use Musician_play or Player_play"
def Musician_play (self, instrument):
return Musician.play (self, instrument)
def Player_play (self, instrument):
return Player.play (self, instrument)
def do_piano (self):
self.play = self.Musician_play
r = Musician.do_piano (self)
del self.play
return r
def do_yoyo (self):
self.play = self.Player_play
r = Player.do_yoyo (self)
del self.play
return r
Since play
is ambiguous, using it becomes error
prone, so we raise an exception. In order to access both playing
functionality, we rename them in an unambiguous way, prefixing
them by their namespace (i.e. the name of the class defining
them). We then have to make do_piano
and
do_yoyo
aware of our renaming.
The solution above is not perfect, because :
-
it is verbose
-
it would not work in more complicated situations
However, it demonstrates the two mechanisms of this package:
-
preventing the ambiguous (i.e. without any namespace) use of
play
-
enabling to associate a namespace to pieces of code
Declaring ambiguous members and assigning namespaces
This package provides the function ambiguous
which creates a dummy member in
the class. This dummy member will therefore be used to assign a
namespace to pieces of code. An implementation of
MusicianPlayer
will look like this:
class FixedMusicianPlayer (Musician, Player):
ambiguous ("play")
play.set_namespace (Musician, Musician.do_piano)
play.set_namespace (Player, Player.do_poker)
Any access to the member play
will raise an AmbiguousMemberException
, unless in a
context which has been assigned a namespace with
play.set_namespace
. Its first parameter is the namespace
to assign, and its second parameter is an object representing a piece
of code (callable, property, class or module).
Note that set_namespace
can be used anywhere, inside or
outside the class definition. It can also be used to assign a namespace
to the line(s) of code just after it:
fmp = FixedMusicPlayer()
FixedMusicPlayer.play.set_namespace (Player) # set NS for next line
fmp.play ("piano") # this call is not ambiguous
For more information, read the doc
.
Automated namespace assignment
Since it will most often be the case that a class uses an
ambiguous member in its own namespace (rather than another class's
namespace), the following lines will be often used:
ambiguous_member.set_namespace (X, X)
ambiguous_member.set_namespace (Y, Y)
where X
and Y
are the classes defining
ambiguous_member
. The function ambiguous
can automatically do this when required. This is achieved by using
the automated
keyword, as in the example below:
class A (object):
def m1 (self): pass
def m2 (self): pass
class B (object):
def m2 (self): pass
def m3 (self): pass
class C (object):
def m3 (self): pass
def m4 (self): pass
class D (A,B,C):
ambiguous ("m2", "m3", automated=True)
# the use of automated above is equivalent to the following lines:
# m2.set_namespace (A, A)
# m2.set_namespace (B, B)
# m3.set_namespace (B, B)
# m3.set_namespace (C, C)
NsAwareClass
Now, considering that ambiguous members can easily be detected by
examining the class, this package also provides the metaclass nsaware_class.NsAwareClass
which will
perform such a detection, as in the example above:
class A (object):
def m1 (self): pass
def m2 (self): pass
class B (object):
def m2 (self): pass
def m3 (self): pass
class C (object):
def m3 (self): pass
def m4 (self): pass
class D (A,B,C):
__metaclass__ = harold.prog.namespace.nsaware_class.NsAwareClass
# the line above does the following:
# ambiguous ("m2", "m3", automated=True)
There is a slight differences, though: instead of raising an AmbiguousMemberException
when trying to
use an ambiguous member, an AmbiguousMemberWarning
is issued.
Inhibiting warnings
In some cases, however, one would want to inhibit the warnings
from NsAwareClass
: when inheriting two classes with the
same protocol, but not explicitely related by inheritance (which
happens in Python, programmers sometimes relying on protocol
rather than inheritance). E.g.:
class Impl (object):
def m (self): print "m"
def n (self): print "n"
class BetterImpl (object):
def m (self): print "better m"
def n (self): print "better n"
class JoinClass (BetterImpl, Impl):
__metaclass__ = NsAwareClass
__override__ = { BetterImpl: Impl }
Though NsAwareClass
would expect the developper of
JoinClass
to state explicitely, for each method, that it
has to use BetterImpl
's implementation, it is possible
to specify this with a single line with the __override__
attribute. This attribute is a dictionary whose keys are overriding
classes, and values are (lists of) overridden classes. In the example
above, it means that any method having its first definition in
BetterImpl
is not ambiguous if the only other definition
is in Impl
.