Our first example of polymorphism dealt with Fractions, an immutable type like other numbers and strings. There are some subtle issues that arise when dealing with mutable objects like container types. Our card classes provide a familiar container type for us to work with.
Let's say we would like to be able to use a plus sign, +, to
add cards to a CardCollection object. Specifically we want to
enable code like this to work,
cc = CardCollection()
card = Card(42)
cc = cc + card
Here's what it would take to be able to use a plus sign in this way,
class CardCollection:
...
def __add__(self, other):
self.cards.append(other)
return self
All we need to do is append the card to the cards in the
CardCollection and return the CardCollection
object. Why do we have to return the whole object? For that matter why do we
have to return anything? Remember what the assignment operation is doing. It
evaluates the expression on the right hand side (RHS), cc +
card, and assigns the result of the expression to the name on the left
hand side, cc. But the RHS operation just calls the function
__add__ so the result of the expression will be the value
returned by __add__. If __add__ doesn't return a
value then the value assigned to cc will be None
— we will have deleted our list! That's why we have to be careful to return the modified object.
Notice that thanks to inheritance we can use the plus sign operator on
Hands and Decks too without writing any new code at all. Score one for inheritance!
roxx = Hand()
roxx = roxx + Card(42)
roxx = roxx + Card(18)
print roxx
will produce the output,
>>> 4 of Spades, 6 of Diamonds >>>
Python's internals are well enough designed that we can chain
+ operations without having to write any additional code,
e.g.
roxx = Hand()
roxx = roxx + Card(42) + Card(13) + Card(2)
print roxx
produces the output,
>>> 4 of Spades, A of Diamonds, 3 of Clubs >>>
This is nice, very nice in fact, but the built-in + operator
does more than this when used with the built-in types. It can add two
integers, but it can also add an integer and a float. So far we are adding a
Card to a CardCollection, but what if we also
wanted to be able to add two CardCollections together? We would
have to modify our code to detect the type of object it is being asked to add
(just like we did with __add__ for Fractions
previously).
def __add__(self, other):
if isinstance(other, Card):
self.cards.append(other)
return self
elif isinstance(other, CardCollection):
self.cards.extend(other.cards)
return self
else:
print 'You can only add cards or other CardCollections to CardCollections!'
With this version we can now write code like this,
roxx = Hand()
roxx = roxx + Card(42) + Card(13) + Card(2)
print 'roxx:', roxx
chris = Hand()
chris = chris + Card(3) + Card(4)
print 'chris:', chris
new = roxx + chris
print 'new:', new
which produces as output,
>>> roxx: 4 of Spades, A of Diamonds, 3 of Clubs chris: 4 of Clubs, 5 of Clubs new: 4 of Spades, A of Diamonds, 3 of Clubs, 4 of Clubs, 5 of Clubs >>>