5.10. OOP Interface

5.10.1. Rationale

  • 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.10.2. Example

>>> class CacheInterface:
...     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

5.10.3. Alternative Notation

>>> class CacheInterface:
...     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
>>> class CacheInterface:
...     def get(self, key: str) -> str: pass
...     def set(self, key: str, value: str) -> None: pass
...     def is_valid(self, key: str) -> bool: pass
>>> class CacheInterface:
...     def get(self, key: str) -> str: ...
...     def set(self, key: str, value: str) -> None: ...
...     def is_valid(self, key: str) -> bool: ...

5.10.4. Use Cases

>>> from datetime import timedelta
>>>
>>>
>>> class Cache:
...     timeout: timedelta
...
...     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
>>>
>>>
>>> class CacheDatabase(Cache):
...     timeout: timedelta
...
...     def is_valid(self, key: str) -> bool:
...         ...
...
...     def get(self, key: str) -> str:
...         ...
...
...     def set(self, key: str, value: str) -> None:
...         ...
>>>
>>>
>>> class CacheRAM(Cache):
...     timeout: timedelta
...
...     def is_valid(self, key: str) -> bool:
...         ...
...
...     def get(self, key: str) -> str:
...         ...
...
...     def set(self, key: str, value: str) -> None:
...         ...
>>>
>>>
>>> class CacheFilesystem(Cache):
...     timeout: timedelta
...
...     def is_valid(self, key: str) -> bool:
...         ...
...
...     def get(self, key: str) -> str:
...         ...
...
...     def set(self, key: str, value: str) -> None:
...         ...
>>>
>>>
>>> fs: Cache = CacheFilesystem()
>>> fs.set('name', 'Mark Watney')
>>> fs.is_valid('name')
>>> fs.get('name')
>>>
>>> ram: Cache = CacheRAM()
>>> ram.set('name', 'Mark Watney')
>>> ram.is_valid('name')
>>> ram.get('name')
>>>
>>> db: Cache = CacheDatabase()
>>> db.set('name', 'Mark Watney')
>>> db.is_valid('name')
>>> db.get('name')

5.10.5. Assignments

Code 5.13. Solution
"""
* Assignment: OOP Interface 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, isclass

    >>> assert isclass(IrisInterface)
    >>> assert hasattr(IrisInterface, '__init__')
    >>> assert hasattr(IrisInterface, 'mean')
    >>> assert hasattr(IrisInterface, 'sum')
    >>> assert hasattr(IrisInterface, 'len')
    >>> assert isfunction(IrisInterface.__init__)
    >>> 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'>}

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

    >>> IrisInterface.mean.__annotations__
    {'return': <class 'float'>}

    >>> IrisInterface.sum.__annotations__
    {'return': <class 'float'>}

    >>> IrisInterface.len.__annotations__
    {'return': <class 'int'>}

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

    >>> IrisInterface.mean(None)
    Traceback (most recent call last):
    NotImplementedError
    >>> IrisInterface.len(None)
    Traceback (most recent call last):
    NotImplementedError
    >>> IrisInterface.sum(None)
    Traceback (most recent call last):
    NotImplementedError
    >>> IrisInterface.__init__(None,1,2,3,4)
    Traceback (most recent call last):
    NotImplementedError
"""


Code 5.14. Solution
"""
* Assignment: OOP Interface Implement
* Complexity: easy
* Lines of code: 3 lines
* Time: 8 min

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

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

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

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

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

    >>> from inspect import isfunction
    >>> 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.__init__.__annotations__  # doctest: +NORMALIZE_WHITESPACE
    {'sepal_length': <class 'float'>,
     'sepal_width': <class 'float'>,
     'petal_length': <class 'float'>,
     'petal_width': <class 'float'>,
     'return': None}

    >>> Setosa.mean.__annotations__
    {'return': <class 'float'>}

    >>> Setosa.sum.__annotations__
    {'return': <class 'float'>}

    >>> Setosa.len.__annotations__
    {'return': <class 'int'>}

    >>> 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