When dealing with multiple types which
are interacting, a program can get particularly messy. For example, consider a
system that parses and executes mathematical expressions. You want to be able to
say Number + Number, Number * Number, etc., where Number is
the base class for a family of numerical objects. But when you say a + b,
and you dont know the exact type of either a or b, so how
can you get them to interact properly?
Add Comment
The answer starts with something you
probably dont think about: Python performs only single dispatching. That
is, if you are performing an operation on more than one object whose type is
unknown, Python can invoke the dynamic binding mechanism on only one of those
types. This doesnt solve the problem, so you end up detecting some types
manually and effectively producing your own dynamic binding behavior.
Add Comment
The solution is called
multiple dispatching.
Remember that polymorphism can occur only via member function calls, so if you
want double dispatching to occur, there must be two member function calls: the
first to determine the first unknown type, and the second to determine the
second unknown type. With multiple dispatching, you must have a polymorphic
method call to determine each of the types. Generally, youll set up a
configuration such that a single member function call produces more than one
dynamic member function call and thus determines more than one type in the
process. To get this effect, you need to work with more than one polymorphic
method call: youll need one call for each dispatch. The methods in the
following example are called compete( ) and eval( ), and
are both members of the same type. (In this case there will be only two
dispatches, which is referred to as
double dispatching). If you
are working with two different type hierarchies that are interacting, then
youll have to have a polymorphic method call in each hierarchy.
Add Comment
Heres an example of multiple
dispatching:
#: c11:PaperScissorsRock.py # Demonstration of multiple dispatching. from __future__ import generators import random # An enumeration type: class Outcome: def __init__(self, value, name): self.value = value self.name = name def __str__(self): return self.name def __eq__(self, other): return self.value == other.value Outcome.WIN = Outcome(0, "win") Outcome.LOSE = Outcome(1, "lose") Outcome.DRAW = Outcome(2, "draw") class Item(object): def __str__(self): return self.__class__.__name__ class Paper(Item): def compete(self, item): # First dispatch: self was Paper return item.evalPaper(self) def evalPaper(self, item): # Item was Paper, we're in Paper return Outcome.DRAW def evalScissors(self, item): # Item was Scissors, we're in Paper return Outcome.WIN def evalRock(self, item): # Item was Rock, we're in Paper return Outcome.LOSE class Scissors(Item): def compete(self, item): # First dispatch: self was Scissors return item.evalScissors(self) def evalPaper(self, item): # Item was Paper, we're in Scissors return Outcome.LOSE def evalScissors(self, item): # Item was Scissors, we're in Scissors return Outcome.DRAW def evalRock(self, item): # Item was Rock, we're in Scissors return Outcome.WIN class Rock(Item): def compete(self, item): # First dispatch: self was Rock return item.evalRock(self) def evalPaper(self, item): # Item was Paper, we're in Rock return Outcome.WIN def evalScissors(self, item): # Item was Scissors, we're in Rock return Outcome.LOSE def evalRock(self, item): # Item was Rock, we're in Rock return Outcome.DRAW def match(item1, item2): print "%s <--> %s : %s" % ( item1, item2, item1.compete(item2)) # Generate the items: def itemPairGen(n): # Create a list of instances of all Items: Items = Item.__subclasses__() for i in range(n): yield (random.choice(Items)(), random.choice(Items)()) for item1, item2 in itemPairGen(20): match(item1, item2) #:~
This was a fairly literal
translation from the Java version, and one of the things you might notice is
that the information about the various combinations is encoded into each type of
Item. It actually ends up being a kind of table, except that it is spread
out through all the classes. This is not very easy to maintain if you ever
expect to modify the behavior or to add a new Item class. Instead, it can
be more sensible to make the table explicit, like this:
Add Comment
#: c11:PaperScissorsRock2.py # Multiple dispatching using a table from __future__ import generators import random class Outcome: def __init__(self, value, name): self.value = value self.name = name def __str__(self): return self.name def __eq__(self, other): return self.value == other.value Outcome.WIN = Outcome(0, "win") Outcome.LOSE = Outcome(1, "lose") Outcome.DRAW = Outcome(2, "draw") class Item(object): def compete(self, item): # Use a tuple for table lookup: return outcome[self.__class__, item.__class__] def __str__(self): return self.__class__.__name__ class Paper(Item): pass class Scissors(Item): pass class Rock(Item): pass outcome = { (Paper, Rock): Outcome.WIN, (Paper, Scissors): Outcome.LOSE, (Paper, Paper): Outcome.DRAW, (Scissors, Paper): Outcome.WIN, (Scissors, Rock): Outcome.LOSE, (Scissors, Scissors): Outcome.DRAW, (Rock, Scissors): Outcome.WIN, (Rock, Paper): Outcome.LOSE, (Rock, Rock): Outcome.DRAW, } def match(item1, item2): print "%s <--> %s : %s" % ( item1, item2, item1.compete(item2)) # Generate the items: def itemPairGen(n): # Create a list of instances of all Items: Items = Item.__subclasses__() for i in range(n): yield (random.choice(Items)(), random.choice(Items)()) for item1, item2 in itemPairGen(20): match(item1, item2) #:~
Its a tribute to the
flexibility of dictionaries that a tuple can be used as a key just as easily as
a single object.
Add Comment
The assumption is that you have a primary
class hierarchy that is fixed; perhaps its from another vendor and you
cant make changes to that hierarchy. However, youd like to add new
polymorphic methods to that hierarchy, which means that normally youd
have to add something to the base class interface. So the dilemma is that you
need to add methods to the base class, but you cant touch the base class.
How do you get around this?
Add Comment
The design pattern that solves this kind
of problem is called a visitor (the final one in the Design
Patterns book), and it builds on the double dispatching scheme shown
in the last section.
Add Comment
The
visitor pattern allows you to extend
the interface of the primary type by creating a separate class hierarchy of type
Visitor to virtualize the operations performed upon the primary type. The
objects of the primary type simply accept the visitor, then call
the visitors dynamically-bound member function.
Add Comment
#: c11:FlowerVisitors.py # Demonstration of "visitor" pattern. from __future__ import generators import random # The Flower hierarchy cannot be changed: class Flower(object): def accept(self, visitor): visitor.visit(self) def pollinate(self, pollinator): print self, "pollinated by", pollinator def eat(self, eater): print self, "eaten by", eater def __str__(self): return self.__class__.__name__ class Gladiolus(Flower): pass class Runuculus(Flower): pass class Chrysanthemum(Flower): pass class Visitor: def __str__(self): return self.__class__.__name__ class Bug(Visitor): pass class Pollinator(Bug): pass class Predator(Bug): pass # Add the ability to do "Bee" activities: class Bee(Pollinator): def visit(self, flower): flower.pollinate(self) # Add the ability to do "Fly" activities: class Fly(Pollinator): def visit(self, flower): flower.pollinate(self) # Add the ability to do "Worm" activities: class Worm(Predator): def visit(self, flower): flower.eat(self) def flowerGen(n): flwrs = Flower.__subclasses__() for i in range(n): yield random.choice(flwrs)() # It's almost as if I had a method to Perform # various "Bug" operations on all Flowers: bee = Bee() fly = Fly() worm = Worm() for flower in flowerGen(10): flower.accept(bee) flower.accept(fly) flower.accept(worm) #:~