3.13. OOP Abstract Interface¶
Python don't have interfaces, although you can achieve similar effect
Since Python 3.8 there are Protocols, which effectively are interfaces
Interfaces cannot be instantiated
Interfaces can be implemented
Implemented class must define all interface methods (implement interface)
Only public method declaration
- interface¶
Software entity with public methods and attribute declaration
- implement¶
Class implements interface if has all public fields and methods from interface
How do you specify and enforce an interface spec in Python?
An interface specification for a module as provided by languages such as C++ and Java describes the prototypes for the methods and functions of the module. Many feel that compile-time enforcement of interface specifications helps in the construction of large programs.
Python 3.0 adds an abc module that lets you define Abstract Base Classes
(ABCs). You can then use isinstance()
and issubclass()
to check
whether an instance or a class implements a particular ABC
. The
collections.abc
module defines a set of useful abstract base classes
such as Iterable
, Container
, and MutableMapping
.
For Python, many of the advantages of interface specifications can be obtained by an appropriate test discipline for components.
A good test suite for a module can both provide a regression test and serve
as a module interface specification and a set of examples. Many Python
modules can be run as a script to provide a simple "self test". Even
modules which use complex external interfaces can often be tested in
isolation using trivial "stub" emulations of the external interface.
The doctest
and unittest
modules or third-party test frameworks
can be used to construct exhaustive test suites that exercise every line
of code in a module.
An appropriate testing discipline can help build large complex applications
in Python as well as having interface specifications would. In fact, it can
be better because an interface specification cannot test certain properties
of a program. For example, the append()
method is expected to add new
elements to the end of some internal list
; an interface specification
cannot test that your append()
implementation will actually do this
correctly, but it's trivial to check this property in a test suite.
Writing test suites is very helpful, and you might want to design your code to make it easily tested. One increasingly popular technique, test-driven development, calls for writing parts of the test suite first, before you write any of the actual code. Of course Python allows you to be sloppy and not write test cases at all.
Note
Source [2]
3.13.1. Problem¶
>>> class DatabaseCache:
... def insert(self, key, value): ...
... def select(self, key): ...
... def delete(self): ...
>>>
>>>
>>> class LocmemCache:
... def store(self, value, key): ...
... def retrieve(self, key): ...
... def purge(self): ...
>>>
>>>
>>> class FilesystemCache:
... def write(self, key, value): ...
... def read(self, key): ...
... def remove(self): ...
Each of those classes has different names for methods which eventually does the same job. This is lack of consistency and common interface:
>>> cache = DatabaseCache()
>>> cache.insert('firstname', 'Mark')
>>> cache.insert('lastname', 'Watney')
>>> cache.select('firstname')
>>> cache.select('lastname')
>>> cache.delete()
>>> cache = LocmemCache()
>>> cache.store('firstname', 'Mark')
>>> cache.store('lastname', 'Watney')
>>> cache.retrieve('firstname')
>>> cache.retrieve('lastname')
>>> cache.purge()
>>> cache = FilesystemCache()
>>> cache.write('firstname', 'Mark')
>>> cache.write('lastname', 'Watney')
>>> cache.read('firstname')
>>> cache.read('lastname')
>>> cache.remove()
3.13.2. Solution¶
S.O.L.I.D.
DIP - Dependency Inversion Principle
Always depend on an abstraction not con
The principle states: High-level modules should not import anything from low-level modules. Both should depend on abstractions (e.g., interfaces). Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.
—SOLID, Dependency Inversion Principle, Robert C. Martin
>>> class ICache:
... def set(self, key: str, value: str) -> None: raise NotImplementedError
... def get(self, key: str) -> str: raise NotImplementedError
... def clear(self) -> None: raise NotImplementedError
>>>
>>>
>>> class DatabaseCache(ICache):
... def set(self, key: str, value: str) -> None: ...
... def get(self, key: str) -> str: ...
... def clear(self) -> None: ...
>>>
>>>
>>> class FilesystemCache(ICache):
... def set(self, key: str, value: str) -> None: ...
... def get(self, key: str) -> str: ...
... def clear(self) -> None: ...
>>>
>>>
>>> class LocmemCache(ICache):
... def set(self, key: str, value: str) -> None: ...
... def get(self, key: str) -> str: ...
... def clear(self) -> None: ...
>>> cache: ICache = DatabaseCache()
>>> cache.set('firstname', 'Mark')
>>> cache.set('lastname', 'Watney')
>>> cache.get('firstname')
>>> cache.get('lastname')
>>> cache.clear()
>>> cache: ICache = FilesystemCache()
>>> cache.set('firstname', 'Mark')
>>> cache.set('lastname', 'Watney')
>>> cache.get('firstname')
>>> cache.get('lastname')
>>> cache.clear()
>>> cache: ICache = LocmemCache()
>>> cache.set('firstname', 'Mark')
>>> cache.set('lastname', 'Watney')
>>> cache.get('firstname')
>>> cache.get('lastname')
>>> cache.clear()
3.13.3. Interface Names¶
Cache
CacheInterface
CacheIface
ICache
Here are some tips for naming interfaces in Python:
1. Use descriptive names: The name of your interface should describe what it does. This makes it easier for other developers to understand your code and use your interface correctly.
2. Use nouns: Interfaces should be named using nouns, as they represent a thing or an object. Avoid using verbs or adjectives in your interface names.
3. Avoid abbreviations: Abbreviations can be confusing and make your code harder to understand. Instead of using abbreviations, use descriptive names that are easy to understand.
5. Use camel case: In Python, it's common to use camel case for naming
interfaces. This means that the first letter of each word in the name is
capitalized. For example, FileReader
uses camel case.
Overall, the key is to choose a descriptive name that accurately reflects the purpose of your interface. This will make it easier for other developers to understand and use your code.
>>> class Cache:
... ...
>>> class CacheInterface:
... ...
>>> class CacheIface:
... ...
>>> class ICache:
... ...
3.13.4. Alternative Notation¶
>>> class ICache:
... def set(self, key: str, value: str) -> None:
... raise NotImplementedError
...
... def get(self, key: str) -> str:
... raise NotImplementedError
...
... def clear(self) -> None:
... raise NotImplementedError
Interfaces do not have any implementation, so you can write them as one-liners. It is a bit more easier to read. You will also focus more on method names and attribute types.
>>> class ICache:
... def set(self, key: str, value: str) -> None: raise NotImplementedError
... def get(self, key: str) -> str: raise NotImplementedError
... def clear(self) -> None: raise NotImplementedError
Sometimes you may get a shorter code, but it will not raise an error in case of implementing class do not cover the name.
>>> class ICache:
... def set(self, key: str, value: str) -> None: pass
... def get(self, key: str) -> str: pass
... def clear(self) -> None: pass
As of three dots (...
) is a valid Python object (Ellipsis) you can write
that:
>>> class ICache:
... def set(self, key: str, value: str) -> None: ...
... def get(self, key: str) -> str: ...
... def clear(self) -> None: ...
3.13.5. Not Existing Notation¶
This currently does not exists in Python
In fact it is not even a valid Python syntax
But it could greatly improve readability
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
3.13.6. Use Case - 0x01¶
>>> class Account:
... def login(self, username: str, password: str) -> None: ...
... def logout(self) -> None: ...
>>>
>>>
>>> class Guest(Account):
... def login(self, username: str, password: str) -> None: ...
... def logout(self) -> None: ...
>>>
>>> class User(Account):
... def login(self, username: str, password: str) -> None: ...
... def logout(self) -> None: ...
>>>
>>> class Admin(Account):
... def login(self, username: str, password: str) -> None: ...
... def logout(self) -> None: ...
3.13.7. Use Case - 0x02¶
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 clear(self) -> None:
... raise NotImplementedError
File cache_impl.py
:
>>> class DatabaseCache(ICache):
... def get(self, key: str) -> str:
... ...
...
... def set(self, key: str, value: str) -> None:
... ...
...
... def clear(self) -> None:
... ...
>>>
>>>
>>> class LocmemCache(ICache):
... def get(self, key: str) -> str:
... ...
...
... def set(self, key: str, value: str) -> None:
... ...
...
... def clear(self) -> None:
... ...
>>>
>>>
>>> class FilesystemCache(ICache):
... def get(self, key: str) -> str:
... ...
...
... def set(self, key: str, value: str) -> None:
... ...
...
... def clear(self) -> None:
... ...
File settings.py
>>> from myapp.cache_iface import ICache
>>> from myapp.cache_impl import DatabaseCache
>>> from myapp.cache_impl import LocmemCache
>>> from myapp.cache_impl import FilesystemCache
>>>
>>>
>>> DefaultCache = LocmemCache
File myapp.py
:
>>> from myapp.settings import DefaultCache, ICache
>>>
>>>
>>> cache: ICache = DefaultCache()
>>> cache.set('firstname', 'Mark')
>>> cache.set('lastname', 'Watney')
>>> cache.get('firstname')
>>> cache.get('lastname')
>>> cache.clear()
Note, that myapp doesn't know which cache is being used. It only depends on configuration in settings file.
3.13.8. Use Case - 0x03¶

Figure 3.6. GIMP (GNU Image Manipulation Project) window with tools and canvas [1]¶
Interface definition with all event handler specification:
>>> class ITool:
... def on_mouse_over(self): raise NotImplementedError
... def on_mouse_out(self): raise NotImplementedError
... def on_mouse_leftbutton(self): raise NotImplementedError
... def on_mouse_rightbutton(self): raise NotImplementedError
... def on_key_press(self): raise NotImplementedError
... def on_key_unpress(self): raise NotImplementedError
Implementation:
>>> class Pencil(ITool):
... def on_mouse_over(self): ...
... def on_mouse_out(self): ...
... def on_mouse_leftbutton(self): ...
... def on_mouse_rightbutton(self): ...
... def on_key_press(self): ...
... def on_key_unpress(self): ...
>>>
>>>
>>> class Pen(ITool):
... def on_mouse_over(self): ...
... def on_mouse_out(self): ...
... def on_mouse_leftbutton(self): ...
... def on_mouse_rightbutton(self): ...
... def on_key_press(self): ...
... def on_key_unpress(self): ...
>>>
>>>
>>> class Brush(ITool):
... def on_mouse_over(self): ...
... def on_mouse_out(self): ...
... def on_mouse_leftbutton(self): ...
... def on_mouse_rightbutton(self): ...
... def on_key_press(self): ...
... def on_key_unpress(self): ...
>>>
>>>
>>> class Eraser(ITool):
... def on_mouse_over(self): ...
... def on_mouse_out(self): ...
... def on_mouse_leftbutton(self): ...
... def on_mouse_rightbutton(self): ...
... def on_key_press(self): ...
... def on_key_unpress(self): ...
3.13.9. References¶
3.13.10. Assignments¶
"""
* 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
"""
"""
* 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 metody
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
"""
* 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