Key Word(s): Polymorphism



What is Polymorphism?

We saw polymorphism last time as the ability to run the same methods on different objects, either through inheritance or by just defining an ad-hoc protocol (duck typing).

The more general definition is:

The ability to write code that looks similar, but operates on different types.

In other words, a single interface serves entities of different types.

Polymorphism Summary

Python type system is strong and dynamic:

  • strong: everything has a well-defined type: type, isinstance
  • dynamic: type is not explicitly declared, changes with content

In classic dynamically typed languages (e.g. Python) most common code is polymorphic.

The types of values are restricted only by explicit runtime checks or errors due to failed support for operations at run time.

Polymorphism is often combined with inheritance, but does not need to be.

One classification of Polymorphism (summarized from Wikipedia) divides it as:

  • on one axis: adhoc, parametric, and subtype based
  • on another axis: dynamic (run time) and static (compile time)

Static vs Dynamic

Dynamic (run-time) polymorphism can be thought of as table based dispatch: that there is, somewhere, atleast conceptually, a table of types, or a linkage of such tables created by inheritance, where implementations are looked up for types.

In static polymorphism (e.g. in C++) the binding to the appropriate class can be done at compile time.

Back to our French Deck Example

In [2]:
import collections
Card = collections.namedtuple('Card', ['rank', 'suit'])
In [3]:
class FrenchDeck():
    ranks = [str(n) for n in range(2, 11)] + list('JQKA') 
    suits = 'spades diamonds clubs hearts'.split()
    
    def __init__(self):
        self._cards = [Card(rank, suit) for suit in self.suits for rank in self.ranks]
                       
    def __len__(self):
        return len(self._cards)
                       
    def __getitem__(self, position): 
        return self._cards[position]
        
In [4]:
mydeck = FrenchDeck()
print(len(mydeck))
52
In [5]:
vars(mydeck)
Out[5]:
{'_cards': [Card(rank='2', suit='spades'),
  Card(rank='3', suit='spades'),
  Card(rank='4', suit='spades'),
  Card(rank='5', suit='spades'),
  Card(rank='6', suit='spades'),
  Card(rank='7', suit='spades'),
  Card(rank='8', suit='spades'),
  Card(rank='9', suit='spades'),
  Card(rank='10', suit='spades'),
  Card(rank='J', suit='spades'),
  Card(rank='Q', suit='spades'),
  Card(rank='K', suit='spades'),
  Card(rank='A', suit='spades'),
  Card(rank='2', suit='diamonds'),
  Card(rank='3', suit='diamonds'),
  Card(rank='4', suit='diamonds'),
  Card(rank='5', suit='diamonds'),
  Card(rank='6', suit='diamonds'),
  Card(rank='7', suit='diamonds'),
  Card(rank='8', suit='diamonds'),
  Card(rank='9', suit='diamonds'),
  Card(rank='10', suit='diamonds'),
  Card(rank='J', suit='diamonds'),
  Card(rank='Q', suit='diamonds'),
  Card(rank='K', suit='diamonds'),
  Card(rank='A', suit='diamonds'),
  Card(rank='2', suit='clubs'),
  Card(rank='3', suit='clubs'),
  Card(rank='4', suit='clubs'),
  Card(rank='5', suit='clubs'),
  Card(rank='6', suit='clubs'),
  Card(rank='7', suit='clubs'),
  Card(rank='8', suit='clubs'),
  Card(rank='9', suit='clubs'),
  Card(rank='10', suit='clubs'),
  Card(rank='J', suit='clubs'),
  Card(rank='Q', suit='clubs'),
  Card(rank='K', suit='clubs'),
  Card(rank='A', suit='clubs'),
  Card(rank='2', suit='hearts'),
  Card(rank='3', suit='hearts'),
  Card(rank='4', suit='hearts'),
  Card(rank='5', suit='hearts'),
  Card(rank='6', suit='hearts'),
  Card(rank='7', suit='hearts'),
  Card(rank='8', suit='hearts'),
  Card(rank='9', suit='hearts'),
  Card(rank='10', suit='hearts'),
  Card(rank='J', suit='hearts'),
  Card(rank='Q', suit='hearts'),
  Card(rank='K', suit='hearts'),
  Card(rank='A', suit='hearts')]}
In [6]:
dir(mydeck)
Out[6]:
['__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__getitem__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__le__',
 '__len__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__',
 '_cards',
 'ranks',
 'suits']
In [7]:
vars(mydeck.__class__)
Out[7]:
mappingproxy({'__dict__': ,
              '__doc__': None,
              '__getitem__': ,
              '__init__': ,
              '__len__': ,
              '__module__': '__main__',
              '__weakref__': ,
              'ranks': ['2',
               '3',
               '4',
               '5',
               '6',
               '7',
               '8',
               '9',
               '10',
               'J',
               'Q',
               'K',
               'A'],
              'suits': ['spades', 'diamonds', 'clubs', 'hearts']})
In [8]:
dir(mydeck.__class__)
Out[8]:
['__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__getitem__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__le__',
 '__len__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__',
 'ranks',
 'suits']

Ad hoc Polymorphism and Object tables

Ad hoc polymorphism is the notion that different functions are called to accomplish the same task for arguments of different types.

This enables the Python Data model with the dunder methods.

If you call len(arg) or iter(arg), we delegate to arg's __len__ or __iter__ by looking them up in the table (class) corresponding to arg.

The net effect is that you get different behaviors for different objects.

You are not looking up a table for the operation but instead looking up a table for the object.

You can think of this as single dispatch: the len is dispatched based on the type of the argument by looking up a table for the argument.

Duck Typing

  • We group together the notion that an object responds to such "messages" into a protocol
  • An example is the informal notion that something is a sequence

This is Duck Typing.

Alex Martelli, the coiner of the phrase Duck Typing, says:

In Python, this mostly boils down to avoiding the use of isinstance to check the object’s type (not to mention the even worse approach of checking, for example, whether type(foo) is bar—which is rightly anathema as it inhibits even the simplest forms of inheritance!).

Tables for dispatching on functions

You can also dispatch a function based on its argument, with no lookup in that argument's table, but rather in a table that is associated with the function. This is also single dispatch, but from a different table.

There is no built in support in Python for this, but you can write it on your own by associating a dictionary with multiple types.

See Chapter 7 (Example 7-20 and Example 7-21) in Fluent Python.

Parametric Polymorphism

Write functions (or types) that are generic "over" other types.

  • This means, for example, a stack that can take either an int or a float or an animal.

    • Notice that this is generally true in a dynamic language such as Python where objects are allocated on the heap and it's the references or labels or ids that are pushed onto the stack.
  • In C++ this can be done using templates at compile time to optimize the allocation of space.

Subtype Polymorphism

This refers to the polymorphism that we encounter in situations where our language provides subclassing.

  • In a language such as C++, this refers to the notion that a dog and a cat can make sounds through an animal pointer.
  • In Python one can use duck typing or inheritance. So subtype polymorphism is then just ad-hoc polymorphism plus an augmented lookup in the inheritance hierarchy.

Object Tables Again

What's this table we keep talking about? We hinted at it earlier when we did:

In [9]:
mydeck.__class__.__dict__
Out[9]:
mappingproxy({'__dict__': ,
              '__doc__': None,
              '__getitem__': ,
              '__init__': ,
              '__len__': ,
              '__module__': '__main__',
              '__weakref__': ,
              'ranks': ['2',
               '3',
               '4',
               '5',
               '6',
               '7',
               '8',
               '9',
               '10',
               'J',
               'Q',
               'K',
               'A'],
              'suits': ['spades', 'diamonds', 'clubs', 'hearts']})

What if we don't find a method in the table?

Either this is a runtime error, or we search in the "parent" classes of this class.

We can see all such attributes by using dir:

In [10]:
dir(mydeck)
Out[10]:
['__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__getitem__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__le__',
 '__len__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__',
 '_cards',
 'ranks',
 'suits']

This works because it gets sent up:

In [11]:
hash(mydeck)
Out[11]:
-9223372036578506990

You can see whats upward of the French Deck by inspecting the Method Order Resolution using the mro method.

In [12]:
FrenchDeck.mro()
Out[12]:
[__main__.FrenchDeck, object]