6.8. Polymorphism

6.8.1. Switch

It all starts with single if statement

language = 'English'

if language == 'Polish':
    result = 'Witaj'
elif language == 'English':
    result = 'Hello'

print(result)
# Hello

It quickly grows into multiple elif:

language = 'English'

if language == 'Polish':
    result = 'Witaj'
elif language == 'English':
    result = 'Hello'
elif language == 'Russian':
    result = 'Привет'
else:
    result = 'Unknown language'

print(result)
# Hello

In other languages you may find switch statement: (note that this is not a valid Python code)

switch(language):
    case 'Polish':
        result = 'Witaj'
    case 'English':
        result = 'Hello'
    case 'Russian':
        result = 'Привет'
    default:
        result = 'Unknown language'

It's a bit cleaner, but essentially the same. Problem is that, switch moves business logic to the execution place:

SWITCH = {'Polish': 'Witaj',
         'English': 'Hello',
         'German': 'Guten Tag'}

language = 'English'

result = SWITCH.get(language, 'Unknown language')
print(result)
# Hello
def switch(key):
    return {
        'Polish': 'Witaj'
        'English': 'Hello',
        'Russian': 'Привет',
    }.get(key, 'Unknown language')

switch('English')
# Hello
switch('Russian')
# Привет

6.8.2. Pattern Matching

  • Since Python 3.10: PEP 636 -- Structural Pattern Matching: Tutorial

>>> language = 'English'
>>>
>>> # doctest: +SKIP
... match language:
...     case 'Polish':
...         result = 'Witaj'
...     case 'English':
...         result = 'Hello'
...     case 'Russian':
...         result = 'Привет'
...     case _:
...         result = 'Unknown language'
>>>
>>> # doctest: +SKIP
... print(result)
Hello
>>> status = 418
>>>
>>> # doctest: +SKIP
... match status:
...     case 400:
...         result = 'Bad request'
...     case 401 | 403 | 405:
...         result = 'Not allowed'
...     case 404:
...         result = 'Not found'
...     case 418:
...         result = "I'm a teapot"
...     case _:
...         result = 'Unexpected status'
>>> request = 'GET /index.html HTTP/2.0'
>>>
>>> # doctest: +SKIP
... match request.split():
...     case ['GET', uri, version]:
...         server.get(uri)
...     case ['POST', uri, version]:
...         server.post(uri)
...     case ['PUT', uri, version]:
...         server.put(uri)
...     case ['DELETE', uri, version]:
...         server.delete(uri)
>>> class Hero:
...     def action():
...         return  ['move', 'left', 20]
>>>
>>> # doctest: +SKIP
... match hero.action():
...     case ['move', ('up'|'down'|'left'|'right') as direction, value]:
...         hero.move(direction, value)
...     case ['make_damage', value]:
...         hero.make_damage(value)
...     case ['take_damage', value]:
...         hero.take_damage(value)
>>> from enum import Enum
>>>
>>> class Key(Enum):
...     ESC = 27
...     ARROW_LEFT = 37
...     ARROW_UP = 38
...     ARROW_RIGHT = 39
...     ARROW_DOWN = 40
>>>
>>> # doctest: +SKIP
... match keyboard.on_key_press():
...     case Key.ESC:
...         game.quit()
...     case Key.ARROW_LEFT:
...         game.move_left()
...     case Key.ARROW_UP:
...         game.move_up()
...     case Key.ARROW_RIGHT:
...         game.move_right()
...     case Key.ARROW_DOWN:
...         game.move_down()
...     case _:
...         raise ValueError(f'Unrecognized key')
>>> from enum import Enum
>>>
>>> class Color(Enum):
...     RED = 0
...     BLUE = 1
...     BLACK = 2
>>>
>>> # doctest: +SKIP
... match color:
...     case Color.RED:
...         print('Soviet')
...     case Color.BLUE:
...         print('Allies')
...     case Color.BLACK:
...         print('Axis')
>>> from enum import Enum
>>>
>>> class SpaceMan(Enum):
...     NASA = 'Astronaut'
...     ESA = 'Astronaut'
...     ROSCOSMOS = 'Cosmonaut'
...     CNSA = 'Taikonaut'
...     ISRO = 'GaganYatri'
>>>
>>> # doctest: +SKIP
... match agency:
...     case SpaceMan.NASA:
...         print('USA')
...     case SpaceMan.ESA:
...         print('Europe')
...     case SpaceMan.ROSCOSMOS:
...         print('Russia')
...     case SpaceMan.CNSA:
...         print('China')
...     case SpaceMan.ISRO:
...         print('India')

6.8.3. Polymorphism

from abc import ABCMeta, abstractmethod
from dataclasses import dataclass


@dataclass
class Person(metaclass=ABCMeta):
    name: str

    @abstractmethod
    def say_hello(self):
        pass


class Astronaut(Person):
    def say_hello(self):
        return f'Hello {self.name}'

class Cosmonaut(Person):
    def say_hello(self):
        return f'Привет {self.name}'


def hello(crew: list[Person]) -> None:
    for member in crew:
        print(member.say_hello())


if __name__ == '__main__':
    crew = [Astronaut('Mark Watney'),
            Cosmonaut('Иван Иванович'),
            Astronaut('Melissa Lewis'),
            Cosmonaut('Jan Twardowski')]

    hello(crew)
# Hello Mark Watney
# Привет Иван Иванович
# Hello Melissa Lewis
# Привет Jan Twardowski

In Python, due to the duck typing and dynamic nature of the language, the Interface or abstract class is not needed to do polymorphism:

from dataclasses import dataclass


@dataclass
class Astronaut:
    name: str

    def say_hello(self):
        return f'Hello {self.name}'

@dataclass
class Cosmonaut:
    name: str

    def say_hello(self):
        return f'Привет {self.name}!'


if __name__ == '__main__':
    crew = [Astronaut('Mark Watney'),
            Cosmonaut('Иван Иванович'),
            Astronaut('Melissa Lewis'),
            Cosmonaut('Jan Twardowski')]

    for member in crew:
        print(member.say_hello())
# Hello Mark Watney
# Привет Иван Иванович
# Hello Melissa Lewis
# Привет Jan Twardowski

6.8.4. Use Cases

UIElement:

from abc import ABCMeta, abstractmethod


class UIElement(metaclass=ABCMeta):
    @abstractmethod
    def draw(self):
        pass

class Input(UIElement):
    def draw(self):
        print('Drawing input')

class Button(UIElement):
    def draw(self):
        print('Drawing button')


def draw(element: UIElement):
    element.draw()


if __name__ == '__main__':
    draw(Textarea())
    draw(Button())

Factory:

DATA = [('Sepal length', 'Sepal width', 'Petal length', 'Petal width', 'Species'),
        (5.8, 2.7, 5.1, 1.9, 'virginica'),
        (5.1, 3.5, 1.4, 0.2, 'setosa'),
        (5.7, 2.8, 4.1, 1.3, 'versicolor'),
        (6.3, 2.9, 5.6, 1.8, 'virginica'),
        (6.4, 3.2, 4.5, 1.5, 'versicolor'),
        (4.7, 3.2, 1.3, 0.2, 'setosa')]


class Iris:
    def __init__(self, sepal_length, sepal_width, petal_length, petal_width):
        self.sepal_length = sepal_length
        self.sepal_width = sepal_width
        self.petal_length = petal_length
        self.petal_width = petal_width

    def __repr__(self):
        name = self.__class__.__name__
        values = tuple(self.__dict__.values())
        return f'\n {name}{values}'


class Setosa(Iris):
    pass

class Virginica(Iris):
    pass

class Versicolor(Iris):
    pass


def factory(species: str):
    if species == 'setosa':
        return Setosa
    if species == 'virginica':
        return Virginica
    if species == 'versicolor':
        return Versicolor


result = []

for *features, species in DATA[1:]:
    iris = factory(species)
    i = iris(*features)
    result.append(i)

print(result)
# [Virginica(5.8, 2.7, 5.1, 1.9),
#  Setosa(5.1, 3.5, 1.4, 0.2),
#  Versicolor(5.7, 2.8, 4.1, 1.3),
#  Virginica(6.3, 2.9, 5.6, 1.8),
#  Versicolor(6.4, 3.2, 4.5, 1.5),
#  Setosa(4.7, 3.2, 1.3, 0.2)]

Dynamic factory:

from dataclasses import dataclass

DATA = [('Sepal length', 'Sepal width', 'Petal length', 'Petal width', 'Species'),
        (5.8, 2.7, 5.1, 1.9, 'virginica'),
        (5.1, 3.5, 1.4, 0.2, 'setosa'),
        (5.7, 2.8, 4.1, 1.3, 'versicolor'),
        (6.3, 2.9, 5.6, 1.8, 'virginica'),
        (6.4, 3.2, 4.5, 1.5, 'versicolor'),
        (4.7, 3.2, 1.3, 0.2, 'setosa')]


@dataclass
class Iris:
    sepal_length: float
    sepal_width: float
    petal_length: float
    petal_width: float

class Setosa(Iris):
    pass

class Virginica(Iris):
    pass

class Versicolor(Iris):
    pass


def factory(species: str):
    species = species.capitalize()
    classes = globals()
    return classes[species]


result = [
    factory(species)(*features)
    for *features, species in DATA[1:]
]

print(result)
# [Virginica(sepal_length=5.8, sepal_width=2.7, petal_length=5.1, petal_width=1.9),
#  Setosa(sepal_length=5.1, sepal_width=3.5, petal_length=1.4, petal_width=0.2),
#  Versicolor(sepal_length=5.7, sepal_width=2.8, petal_length=4.1, petal_width=1.3),
#  Virginica(sepal_length=6.3, sepal_width=2.9, petal_length=5.6, petal_width=1.8),
#  Versicolor(sepal_length=6.4, sepal_width=3.2, petal_length=4.5, petal_width=1.5),
#  Setosa(sepal_length=4.7, sepal_width=3.2, petal_length=1.3, petal_width=0.2)]

6.8.5. Assignments

Todo

Create assignments