Package harold :: Package prog :: Package namespace
[show private | hide private]
[frames | no frames]

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: 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 : However, it demonstrates the two mechanisms of this package:

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.
Submodules

Generated by Epydoc 2.1 on Mon Dec 18 15:25:58 2006 http://epydoc.sf.net