5.14. OOP Abstract Interface

  • Python don't have interfaces

  • Cannot instantiate

  • Inheriting class must implement all methods

  • Only method declaration

  • Since Python 3.8: PEP 544 -- Protocols: Structural subtyping (static duck typing)

interface

Software entity with public methods and attribute declaration

implement

Class implements interface if has all public fields and methods from interface

5.14.1. Syntax

  • Names: Cache, CacheInterface, ICache, CacheIface

>>> class CacheInterface:
...     def set(self, key: str, value: str) -> None:
...         raise NotImplementedError
...
...     def get(self, key: str) -> str:
...         raise NotImplementedError
...
...     def is_valid(self, key: str) -> bool:
...         raise NotImplementedError

5.14.2. Alternative Notation

>>> class CacheInterface:
...     def set(self, key: str, value: str) -> None: raise NotImplementedError
...     def get(self, key: str) -> str: raise NotImplementedError
...     def is_valid(self, key: str) -> bool: raise NotImplementedError

Sometimes you may get a shorter code, but it will not raise an error.

>>> class CacheInterface:
...     def set(self, key: str, value: str) -> None: pass
...     def get(self, key: str) -> str: pass
...     def is_valid(self, key: str) -> bool: pass

As of three dots (...) is a valid Python object (Ellipsis) you can write that:

>>> class CacheInterface:
...     def set(self, key: str, value: str) -> None: ...
...     def get(self, key: str) -> str: ...
...     def is_valid(self, key: str) -> bool: ...

The following code is not a valid Python syntax... How nice it would be to write:

>>> @interface 
... class Cache:
...     def set(self, key: str, value: str) -> None: ...
...     def get(self, key: str) -> str: ...
...     def is_valid(self, key: str) -> bool: ...
>>> class Cache(interface=True): 
...     def set(self, key: str, value: str) -> None: ...
...     def get(self, key: str) -> str: ...
...     def is_valid(self, key: str) -> bool: ...
>>> interface Cache: 
...     def set(self, key: str, value: str) -> None
...     def get(self, key: str) -> str
...     def is_valid(self, key: str) -> bool

5.14.3. Example

>>> class ICache:
...     def set(self, key: str, value: str) -> None: ...
...     def get(self, key: str) -> str: ...
...     def is_valid(self, key: str) -> bool: ...
>>>
>>>
>>> class DatabaseCache(ICache):
...      ...
>>>
>>> class InMemoryCache(ICache):
...      ...
>>>
>>> class FilesystemCache(ICache):
...      ...
>>> mycache: ICache = DatabaseCache()
>>> mycache.set('firstname', 'Mark')
>>> mycache.is_valid('firstname')
>>> mycache.is_valid('lastname')
>>> mycache.get('firstname')
>>> mycache: ICache = InMemoryCache()
>>> mycache.set('firstname', 'Mark')
>>> mycache.is_valid('firstname')
>>> mycache.is_valid('lastname')
>>> mycache.get('firstname')
>>> mycache: ICache = FilesystemCache()
>>> mycache.set('firstname', 'Mark')
>>> mycache.is_valid('firstname')
>>> mycache.is_valid('lastname')
>>> mycache.get('firstname')

5.14.4. Use Case - 0x01

  • Cache

File cache_iface.py:

>>> class ICache:
...     def get(self, key: str) -> str:
...         raise NotImplementedError
...
...     def set(self, key: str, value: str) -> None:
...         raise NotImplementedError
...
...     def is_valid(self, key: str) -> bool:
...         raise NotImplementedError

File cache_impl.py:

>>> class DatabaseCache(ICache):
...     def is_valid(self, key: str) -> bool:
...         ...
...
...     def get(self, key: str) -> str:
...         ...
...
...     def set(self, key: str, value: str) -> None:
...         ...
>>>
>>>
>>> class InMemoryCache(ICache):
...     def is_valid(self, key: str) -> bool:
...         ...
...
...     def get(self, key: str) -> str:
...         ...
...
...     def set(self, key: str, value: str) -> None:
...         ...
>>>
>>>
>>> class FilesystemCache(ICache):
...     def is_valid(self, key: str) -> bool:
...         ...
...
...     def get(self, key: str) -> str:
...         ...
...
...     def set(self, key: str, value: str) -> None:
...         ...

File settings.py

>>> from myapp.cache_iface import ICache  
>>> from myapp.cache_impl import DatabaseCache  
>>> from myapp.cache_impl import InMemoryCache  
>>> from myapp.cache_impl import FilesystemCache  
>>>
>>>
>>> DefaultCache = InMemoryCache

File myapp.py:

>>> from myapp.settings import DefaultCache, ICache  
>>>
>>>
>>> cache: ICache = DefaultCache()
>>> cache.set('name', 'Mark Watney')
>>> cache.is_valid('name')
>>> cache.get('name')

Note, that myapp doesn't know which cache is being used. It only depends on configuration in settings file.

5.14.5. Use Case - 0x02

../../_images/oop-interface-gimp.jpg

Figure 5.1. GIMP (GNU Image Manipulation Project) window with tools and canvas 1

>>> class Tool:
...     def on_mouse_over(self): raise NotImplementedError
...     def on_mouse_out(self): raise NotImplementedError
...     def on_mouse_click_leftbutton(self): raise NotImplementedError
...     def on_mouse_unclick_leftbutton(self): raise NotImplementedError
...     def on_mouse_click_rightbutton(self): raise NotImplementedError
...     def on_mouse_unclick_rightbutton(self): raise NotImplementedError
...     def on_key_press(self): raise NotImplementedError
...     def on_key_unpress(self): raise NotImplementedError
>>>
>>>
>>> class Pencil(Tool):
...     def on_mouse_over(self):
...         ...
...
...     def on_mouse_out(self):
...         ...
...
...     def on_mouse_click_leftbutton(self):
...         ...
...
...     def on_mouse_unclick_leftbutton(self):
...         ...
...
...     def on_mouse_click_rightbutton(self):
...         ...
...
...     def on_mouse_unclick_rightbutton(self):
...         ...
...
...     def on_key_press(self):
...         ...
...
...     def on_key_unpress(self):
...         ...
>>>
>>>
>>> class Pen(Tool):
...     def on_mouse_over(self):
...         ...
...
...     def on_mouse_out(self):
...         ...
...
...     def on_mouse_click_leftbutton(self):
...         ...
...
...     def on_mouse_unclick_leftbutton(self):
...         ...
...
...     def on_mouse_click_rightbutton(self):
...         ...
...
...     def on_mouse_unclick_rightbutton(self):
...         ...
...
...     def on_key_press(self):
...         ...
...
...     def on_key_unpress(self):
...         ...

5.14.6. References

1

Download GIMP. Year: 2022. Retrieved: 2022-08-11. URL: https://anderbot.com/wp-content/uploads/2020/10/GIMP5.jpg

5.14.7. Assignments

Code 5.23. Solution
"""
* Assignment: OOP AbstractInterface Define
* Complexity: easy
* Lines of code: 13 lines
* Time: 8 min

English:
    1. Define interface `IrisInterface`
    2. Attributes: `sepal_length, sepal_width, petal_length, petal_width`
    3. Methods: `sum()`, `len()`, `mean()` in `IrisInterface`
    4. All methods and constructor must raise exception `NotImplementedError`
    5. Run doctests - all must succeed

Polish:
    1. Zdefiniuj interfejs `IrisInterface`
    2. Attributes: `sepal_length, sepal_width, petal_length, petal_width`
    3. Metody: `sum()`, `len()`, `mean()` w `IrisInterface`
    4. Wszystkie metody oraz konstruktor muszą podnosić wyjątek `NotImplementedError`
    5. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> import sys; sys.tracebacklimit = 0
    >>> from inspect import isfunction

    >>> assert hasattr(IrisInterface, 'mean')
    >>> assert hasattr(IrisInterface, 'sum')
    >>> assert hasattr(IrisInterface, 'len')

    >>> assert isfunction(IrisInterface.mean)
    >>> assert isfunction(IrisInterface.sum)
    >>> assert isfunction(IrisInterface.len)

    >>> IrisInterface.__annotations__  # doctest: +NORMALIZE_WHITESPACE
    {'sepal_length': <class 'float'>,
     'sepal_width': <class 'float'>,
     'petal_length': <class 'float'>,
     'petal_width': <class 'float'>}

    >>> iris = IrisInterface(5.8, 2.7, 5.1, 1.9)
    Traceback (most recent call last):
    NotImplementedError
"""


Code 5.24. Solution
"""
* Assignment: OOP AbstractInterface Implement
* Complexity: easy
* Lines of code: 12 lines
* Time: 5 min

English:
    1. Define class `Setosa` implementing `IrisInterface`
    2. Implement methods
    3. Run doctests - all must succeed

Polish:
    1. Stwórz klasę `Setosa` implementującą `IrisInterface`
    2. Zaimplementuj metory
    3. Uruchom doctesty - wszystkie muszą się powieść

Hints:
    * `vars(self).values()`
    * `mean = sum() / len()`

Tests:
    >>> import sys; sys.tracebacklimit = 0
    >>> from inspect import isfunction

    >>> assert issubclass(Setosa, IrisInterface)
    >>> assert hasattr(Setosa, 'mean')
    >>> assert hasattr(Setosa, 'sum')
    >>> assert hasattr(Setosa, 'len')

    >>> assert isfunction(Setosa.mean)
    >>> assert isfunction(Setosa.sum)
    >>> assert isfunction(Setosa.len)

    >>> Setosa.__annotations__  # doctest: +NORMALIZE_WHITESPACE
    {'sepal_length': <class 'float'>,
     'sepal_width': <class 'float'>,
     'petal_length': <class 'float'>,
     'petal_width': <class 'float'>}

    >>> setosa = Setosa(5.1, 3.5, 1.4, 0.2)
    >>> setosa.len()
    4
    >>> setosa.sum()
    10.2
    >>> setosa.mean()
    2.55
"""

class IrisInterface:
    sepal_length: float
    sepal_width: float
    petal_length: float
    petal_width: float

    def __init__(self,
                 sepal_length: float,
                 sepal_width: float,
                 petal_length: float,
                 petal_width: float) -> None:
        raise NotImplementedError

    def mean(self) -> float:
        raise NotImplementedError

    def sum(self) -> float:
        raise NotImplementedError

    def len(self) -> int:
        raise NotImplementedError


Code 5.25. Solution
"""
* Assignment: OOP AbstractInterface Values
* Complexity: easy
* Lines of code: 12 lines
* Time: 8 min

English:
    1. Define class `Setosa` implementing `IrisInterface`
    2. Implement methods
    3. Note, that attribute `species` is a `str`, and in Python you cannot add `str` and `float`
    4. Create protected method `_get_values()` which returns values of `int` and `float` type attibutes
    5. Why this method is not in interface?
    6. Run doctests - all must succeed

Polish:
    1. Stwórz klasę `Setosa` implementującą `IrisInterface`
    2. Zaimplementuj metody
    3. Zwróć uwagę, że atrybut `species` jest `str`, a Python nie można dodawać `str` i `float`
    4. Stwórz metodę chronioną `_get_values()`, która zwraca wartości atrybutów typu `int` i `float`
    5. Dlaczego ta metoda nie jest w interfejsie?
    6. Uruchom doctesty - wszystkie muszą się powieść

Hints:
    * `var(self).values()`
    * `instanceof()` or `type()`
    * `mean = sum() / len()`

Tests:
    >>> import sys; sys.tracebacklimit = 0
    >>> from inspect import isfunction

    >>> assert issubclass(Setosa, IrisInterface)
    >>> assert hasattr(Setosa, 'mean')
    >>> assert hasattr(Setosa, 'sum')
    >>> assert hasattr(Setosa, 'len')

    >>> assert isfunction(Setosa.mean)
    >>> assert isfunction(Setosa.sum)
    >>> assert isfunction(Setosa.len)

    >>> Setosa.__annotations__  # doctest: +NORMALIZE_WHITESPACE
    {'sepal_length': <class 'float'>,
     'sepal_width': <class 'float'>,
     'petal_length': <class 'float'>,
     'petal_width': <class 'float'>,
     'species': <class 'str'>}

    >>> setosa = Setosa(5.1, 3.5, 1.4, 0.2, 'setosa')
    >>> setosa.len()
    4
    >>> setosa.sum()
    10.2
    >>> setosa.mean()
    2.55
"""

class IrisInterface:
    sepal_length: float
    sepal_width: float
    petal_length: float
    petal_width: float
    species: str

    def __init__(self,
                 sepal_length: float,
                 sepal_width: float,
                 petal_length: float,
                 petal_width: float,
                 species: str) -> None:
        raise NotImplementedError

    def mean(self) -> float:
        raise NotImplementedError

    def sum(self) -> float:
        raise NotImplementedError

    def len(self) -> int:
        raise NotImplementedError