5.19. OOP Abstract Polymorphism

5.19.1. Procedural Polymorphism

  • UNIX getchar() function used function lookup table with pointers

>>> keyboard = {
...     'open': lambda: ...,
...     'close':  lambda: ...,
...     'read':  lambda bytes: ...,
...     'write':  lambda content: ...,
...     'seek':  lambda position: ...,
... }
>>>
>>> file = {
...     'open': lambda: ...,
...     'close':  lambda: ...,
...     'read':  lambda bytes: ...,
...     'write':  lambda content: ...,
...     'seek':  lambda position: ...,
... }
>>>
>>> socket = {
...     'open': lambda: ...,
...     'close':  lambda: ...,
...     'read':  lambda bytes: ...,
...     'write':  lambda content: ...,
...     'seek':  lambda position: ...,
... }
>>>
>>>
>>> def getchar(obj):
...     obj['open']()
...     obj['seek'](0)
...     obj['read'](1)
...     obj['close']()
>>>
>>>
>>> getchar(file)
>>> getchar(keyboard)
>>> getchar(socket)

5.19.2. Elif

It all starts with single if statement

>>> language = 'English'
>>>
>>> if language == 'English':
...     result = 'Hello'
>>>
>>> print(result)
Hello
>>> language = 'English'
>>>
>>> if language == 'English':
...     result = 'Hello'
... elif language == 'Polish':
...     result = 'Witaj'
>>>
>>> print(result)
Hello

It quickly grows into multiple elif:

>>> language = 'English'
>>>
>>> if language == 'English':
...     result = 'Hello'
... elif language == 'Polish':
...     result = 'Witaj'
... elif language == 'Spanish':
...     result = 'Hola'
... else:
...     result = 'Unknown language'
>>>
>>> print(result)
Hello

5.19.3. Switch

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

>>> switch(language):  
...     case 'English':  result = 'Hello'; break;
...     case 'Polish':   result = 'Witaj'; break;
...     case 'Spanish':  result = 'Hola'; break;
...     default:         result = 'Unknown language'; break;

Problem is that, switch moves business logic to the execution place. You can write it in a function using dict and .get() method with default value. It's a bit cleaner, but essentially the same...

>>> def switch(key):
...     return {
...         'English': 'Hello',
...         'Polish': 'Witaj',
...         'Spanish': 'Hola',
...     }.get(key, 'Unknown language')
>>>
>>> switch('English')
'Hello'
>>> switch('Spanish')
'Hola'

5.19.4. Pattern Matching

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

  • More information Conditional Syntax Match 1

>>> language = 'English'
>>>
>>> match language:
...     case 'English':  result = 'Hello'
...     case 'Polish':   result = 'Witaj'
...     case 'Spanish':  result = 'Hola'
...     case _:          result = 'Unknown language'

5.19.5. Polymorphism

>>> from abc import ABC, abstractmethod
>>>
>>>
>>> class UIElement(ABC):
...     def __init__(self, name):
...         self.name = name
...
...     @abstractmethod
...     def render(self):
...         pass
>>>
>>>
>>> class TextInput(UIElement):
...     def render(self):
...         print(f'Rendering {self.name} TextInput')
>>>
>>>
>>> class Button(UIElement):
...     def render(self):
...         print(f'Rendering {self.name} Button')
>>>
>>>
>>> def render(component: list[UIElement]):
...     for element in component:
...         element.render()
>>>
>>>
>>> login_window = [
...     TextInput(name='Username'),
...     TextInput(name='Password'),
...     Button(name='Submit'),
... ]
>>>
>>> render(login_window)
Rendering Username TextInput
Rendering Password TextInput
Rendering Submit Button

5.19.6. Use Case - 0x01

>>> from abc import ABC, abstractmethod
>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass
... class Person(ABC):
...     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'Witaj {self.name}'
>>>
>>>
>>> def hello(crew: list[Person]) -> None:
...     for member in crew:
...         print(member.say_hello())
>>>
>>>
>>> crew = [Astronaut('Mark Watney'),
...         Cosmonaut('José Jiménez'),
...         Astronaut('Melissa Lewis'),
...         Cosmonaut('Pan Twardowski')]
>>>
>>> hello(crew)
Hello Mark Watney
Witaj José Jiménez
Hello Melissa Lewis
Witaj Pan 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'Witaj {self.name}'
>>>
>>>
>>> crew = [Astronaut('Mark Watney'),
...         Cosmonaut('José Jiménez'),
...         Astronaut('Melissa Lewis'),
...         Cosmonaut('Pan Twardowski')]
>>>
>>> for member in crew:
...     print(member.say_hello())
Hello Mark Watney
Witaj José Jiménez
Hello Melissa Lewis
Witaj Pan Twardowski

5.19.7. Use Case - 0x02

  • Login Window

>>> import re
>>>
>>>
>>> class UIElement:
...     def __init__(self, name):
...         self.name = name
...
...     def on_mouse_hover(self):
...         raise NotImplementedError
...
...     def on_mouse_out(self):
...         raise NotImplementedError
...
...     def on_mouse_click(self):
...         raise NotImplementedError
...
...     def on_key_press(self):
...         raise NotImplementedError
...
...     def render(self):
...         raise NotImplementedError
>>>
>>>
>>> class Button(UIElement):
...     action: str
...
...     def __init__(self, *args, action: str | None = None, **kwargs):
...         self.action = action
...         super().__init__(*args, **kwargs)
...
...     def on_key_press(self):
...         pass
...
...     def on_mouse_hover(self):
...         pass
...
...     def on_mouse_out(self):
...         pass
...
...     def on_mouse_click(self):
...         pass
...
...     def render(self):
...         action = self.action
...         print(f'Rendering Button with {action}')
>>>
>>>
>>> class Input(UIElement):
...     regex: re.Pattern
...
...     def __init__(self, *args, regex: str | None = None, **kwargs):
...         self.regex = re.compile(regex)
...         super().__init__(*args, **kwargs)
...
...     def on_key_press(self):
...         pass
...
...     def on_mouse_hover(self):
...         pass
...
...     def on_mouse_out(self):
...         pass
...
...     def on_mouse_click(self):
...         pass
...
...     def render(self):
...         regex = self.regex
...         print(f'Rendering Input with {regex}')
>>>
>>>
>>> def render(components: list[UIElement]):
...     for obj in components:
...         obj.render()
>>>
>>>
>>> login_window = [
...     Input('Username', regex='[a-zA-Z0-9]'),
...     Input('Password', regex='[a-zA-Z0-9!@#$%^&*()]'),
...     Button('Submit', action='/login.html'),
... ]
>>>
>>> render(login_window)
Rendering Input with re.compile('[a-zA-Z0-9]')
Rendering Input with re.compile('[a-zA-Z0-9!@#$%^&*()]')
Rendering Button with /login.html

5.19.8. References

1

https://python.astrotech.io/basics/conditional/syntax-match.html

5.19.9. Assignments