5.16. OOP Abstract Class

  • Since Python 3.0: PEP 3119 -- Introducing Abstract Base Classes

  • Cannot instantiate

  • Possible to indicate which method must be implemented by child

  • Inheriting class must implement all methods

  • Some methods can have implementation

  • Python Abstract Base Classes 1

abstract class

Class which can only be inherited, not instantiated

abstract method

Method must be implemented in a subclass

abstract static method

Static method which must be implemented in a subclass

5.16.1. Syntax

  • New class ABC has ABCMeta as its meta class

  • Using ABC as a base class has essentially the same effect as specifying metaclass=abc.ABCMeta, but is simpler to type and easier to read

  • abc.ABC basically just an extra layer over metaclass=abc.ABCMeta

  • abc.ABC implicitly defines the metaclass for you

>>> from abc import ABC, abstractmethod
>>>
>>>
>>> class MyClass(ABC):
...
...     @abstractmethod
...     def mymethod(self):
...         pass

5.16.2. Abstract Method

>>> from abc import ABC, abstractmethod
>>>
>>>
>>> class Astronaut(ABC):
...     @abstractmethod
...     def say_hello(self):
...         pass
>>>
>>>
>>> astro = Astronaut()
Traceback (most recent call last):
TypeError: Can't instantiate abstract class Astronaut with abstract method say_hello

5.16.3. Abstract Property

  • abc.abstractproperty is deprecated since Python 3.3

  • Use property with abc.abstractmethod instead

>>> from abc import ABC, abstractproperty
>>>
>>>
>>> class Monster(ABC):
...     @abstractproperty
...     def DAMAGE(self) -> int:
...         pass
>>>
>>>
>>> class Dragon(Monster):
...     DAMAGE: int = 10
>>> from abc import ABC, abstractmethod
>>>
>>>
>>> class Monster(ABC):
...     @property
...     @abstractmethod
...     def DAMAGE(self) -> int:
...         pass
>>>
>>>
>>> class Dragon(Monster):
...     DAMAGE: int = 10
>>> from abc import ABC, abstractproperty
>>>
>>>
>>> class Monster(ABC):
...     @abstractproperty
...     def DAMAGE_MIN(self):
...         pass
...
...     @abstractproperty
...     def DAMAGE_MAX(self):
...         pass
>>>
>>>
>>> class Dragon(Monster):
...     DAMAGE_MIN: int = 10
...     DAMAGE_MAX: int = 20

5.16.4. Common Problems

In order to use Abstract Base Class you must create abstract method. Otherwise it won't prevent from instantiating:

>>> from abc import ABC
>>>
>>>
>>> class Astronaut(ABC):
...     pass
>>>
>>>
>>> astro = Astronaut()   # It will not raise an error, because there are no abstractmethods
>>>
>>> print('no errors')
no errors

The Human class does not inherits from ABC or has metaclass=ABCMeta:

>>> from abc import abstractmethod
>>>
>>>
>>> class Human:
...     @abstractmethod
...     def get_name(self):
...         pass
>>>
>>>
>>> class Astronaut(Human):
...     pass
>>>
>>>
>>> astro = Astronaut()  # None abstractmethod is implemented in child class
>>>
>>> print('no errors')
no errors

Must implement all abstract methods:

>>> from abc import ABC, abstractmethod
>>>
>>>
>>> class Human(ABC):
...     @abstractmethod
...     def get_name(self):
...         pass
...
...     @abstractmethod
...     def set_name(self):
...         pass
>>>
>>>
>>> class Astronaut(Human):
...     pass
>>>
>>>
>>> astro = Astronaut()  # None abstractmethod is implemented in child class
Traceback (most recent call last):
TypeError: Can't instantiate abstract class Astronaut with abstract methods get_name, set_name

All abstract methods must be implemented in child class:

>>> from abc import ABC, abstractmethod
>>>
>>>
>>> class Human(ABC):
...     @abstractmethod
...     def get_name(self):
...         pass
...
...     @abstractmethod
...     def set_name(self):
...         pass
>>>
>>>
>>> class Astronaut(Human):
...     def get_name(self):
...         return 'Mark Watney'
>>>
>>>
>>> astro = Astronaut()  # There are abstractmethods without implementation
Traceback (most recent call last):
TypeError: Can't instantiate abstract class Astronaut with abstract method set_name

Problem - Child class has no abstract attribute (using abstractproperty):

>>> from abc import ABC, abstractproperty
>>>
>>>
>>> class Monster(ABC):
...     @abstractproperty
...     def DAMAGE(self) -> int:
...         pass
>>>
>>> class Dragon(Monster):
...     pass
>>>
>>>
>>> d = Dragon()
Traceback (most recent call last):
TypeError: Can't instantiate abstract class Dragon with abstract method DAMAGE

Problem - Child class has no abstract attribute (using property and abstractmethod):

>>> from abc import ABC, abstractmethod
>>>
>>>
>>> class Monster(ABC):
...     @property
...     @abstractmethod
...     def DAMAGE(self) -> int:
...         pass
>>>
>>> class Dragon(Monster):
...     pass
>>>
>>>
>>> d = Dragon()
Traceback (most recent call last):
TypeError: Can't instantiate abstract class Dragon with abstract method DAMAGE

Problem - Despite having defined property, the order of decorators (abstractmethod and property is invalid). Should be reversed: first @property then @abstractmethod:

>>> from abc import ABC, abstractmethod
>>>
>>>
>>> class Monster(ABC):
...     @property
...     @abstractmethod
...     def DAMAGE(self) -> int:
...         pass
>>>
>>>
>>> class Dragon(Monster):
...     DAMAGE: int = 10
>>>
>>>
>>> d = Dragon()

abc is common name and it is very easy to create file, variable lub module with the same name as the library, hence overwrite it. In case of error. Check all entries in sys.path or sys.modules['abc'] to find what is overwriting it:

>>> from pprint import pprint
>>> import sys
>>>
>>>
>>> sys.modules['abc']  
<module 'abc' from '...'>
>>>
>>> pprint(sys.path)  
['/Users/watney/myproject',
 '/Applications/PyCharm 2022.1.app/Contents/plugins/python/helpers/pydev',
 '/Applications/PyCharm 2022.1.app/Contents/plugins/python/helpers/pycharm_display',
 '/Applications/PyCharm 2022.1.app/Contents/plugins/python/helpers/third_party/thriftpy',
 '/Applications/PyCharm 2022.1.app/Contents/plugins/python/helpers/pydev',
 '/Applications/PyCharm 2022.1.app/Contents/plugins/python/helpers/pycharm_matplotlib_backend',
 '/usr/local/Cellar/python@3.10/3.10.2/Frameworks/Python.framework/Versions/3.10/lib/python310.zip',
 '/usr/local/Cellar/python@3.10/3.10.2/Frameworks/Python.framework/Versions/3.10/lib/python3.10',
 '/usr/local/Cellar/python@3.10/3.10.2/Frameworks/Python.framework/Versions/3.10/lib/python3.10/lib-dynload',
 '/Users/watney/myproject/venv-3.10/lib/python3.10/site-packages']

5.16.5. Use Case - 0x01

Abstract Class:

>>> from abc import ABC, abstractmethod
>>>
>>>
>>> class Document(ABC):
...     def __init__(self, filename):
...         self.filename = filename
...
...     @abstractmethod
...     def display(self):
...         pass
>>>
>>>
>>> class PDFDocument(Document):
...     def display(self):
...         """display file content as PDF Document"""
>>>
>>> class WordDocument(Document):
...     def display(self):
...         """display file content as Word Document"""
>>>
>>>
>>> file1 = PDFDocument('myfile.pdf')
>>> file1.display()
>>>
>>> file2 = Document('myfile.txt')
Traceback (most recent call last):
TypeError: Can't instantiate abstract class Document with abstract method display

5.16.6. Use Case - 0x02

>>> 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.16.7. Use Case - 0x03

>>> class Person(ABC):
...     age: int
...
...     @property
...     @abstractmethod
...     def AGE_MAX(self) -> int: ...
...
...     @abstractproperty
...     def AGE_MIN(self) -> int: ...
...
...     def __init__(self, age):
...         if not self.AGE_MIN <= age < self.AGE_MAX:
...             raise ValueError('Age is out of bounds')
...         self.age = age
>>>
>>>
>>> class Astronaut(Person):
...     AGE_MIN = 30
...     AGE_MAX = 50
>>>
>>>
>>> mark = Astronaut(age=40)

5.16.8. Further Reading

5.16.9. References

1

https://docs.python.org/3/library/collections.abc.html

5.16.10. Assignments

Code 5.26. Solution
"""
* Assignment: OOP AbstractClass Syntax
* Complexity: easy
* Lines of code: 10 lines
* Time: 5 min

English:
    1. Create abstract class `IrisAbstract`
    2. Create abstract method `get_name()` in `IrisAbstract`
    3. Create class `Setosa` inheriting from `IrisAbstract`
    4. Try to create instance of a class `Setosa`
    5. Try to create instance of a class `IrisAbstract`
    6. Run doctests - all must succeed

Polish:
    1. Stwórz klasę abstrakcyjną `IrisAbstract`
    2. Stwórz metodę abstrakcyjną `get_name()` w `IrisAbstract`
    3. Stwórz klasę `Setosa` dziedziczące po `IrisAbstract`
    4. Spróbuj stworzyć instancje klasy `Setosa`
    5. Spróbuj stworzyć instancję klasy `IrisAbstract`
    6. Uruchom doctesty - wszystkie muszą się powieść

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

    >>> assert isclass(IrisAbstract)
    >>> assert isclass(Setosa)
    >>> assert isabstract(IrisAbstract)
    >>> assert not isabstract(Setosa)
    >>> assert hasattr(IrisAbstract, 'get_name')
    >>> assert hasattr(Setosa, 'get_name')
    >>> assert not hasattr(Setosa.get_name, '__isabstractmethod__')
    >>> assert hasattr(IrisAbstract.get_name, '__isabstractmethod__')
    >>> assert IrisAbstract.get_name.__isabstractmethod__ == True

    >>> iris = IrisAbstract()
    Traceback (most recent call last):
    TypeError: Can't instantiate abstract class IrisAbstract with abstract method get_name
    >>> setosa = Setosa()
    >>> assert ismethod(setosa.get_name)

Warning:
    * Last line of doctest, second to last word of `TypeError` message
    * In Python 3.7, 3.8 there is "methods" word in doctest
    * In Python 3.9, 3.10 there is "method" word in doctest
    * So it differs by "s" at the end of "method" word
"""

Code 5.27. Solution
"""
* Assignment: OOP AbstractClass Interface
* Complexity: easy
* Lines of code: 11 lines
* Time: 5 min

English:
    1. Define abstract class `IrisAbstract`
    2. Abstract methods: `__init__`, `sum()`, `len()`, `mean()`
    3. Run doctests - all must succeed

Polish:
    1. Zdefiniuj klasę abstrakcyjną `IrisAbstract`
    2. Metody abstrakcyjne: `__init__`, `sum()`, `len()`, `mean()`
    3. Uruchom doctesty - wszystkie muszą się powieść

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

    >>> assert isclass(IrisAbstract)
    >>> assert isabstract(IrisAbstract)
    >>> assert hasattr(IrisAbstract, '__init__')
    >>> assert hasattr(IrisAbstract, 'mean')
    >>> assert hasattr(IrisAbstract, 'sum')
    >>> assert hasattr(IrisAbstract, 'len')
    >>> assert IrisAbstract.__init__.__isabstractmethod__ == True
    >>> assert IrisAbstract.mean.__isabstractmethod__ == True
    >>> assert IrisAbstract.sum.__isabstractmethod__ == True
    >>> assert IrisAbstract.len.__isabstractmethod__ == True

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

from abc import ABC, abstractmethod


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

# Define abstract class `IrisAbstract`
# Abstract methods: `__init__`, `sum()`, `len()`, `mean()`


Code 5.28. Solution
"""
* Assignment: OOP AbstractClass Annotate
* Complexity: easy
* Lines of code: 5 lines
* Time: 3 min

English:
    1. Modify abstract class `IrisAbstract`
    2. Add type annotation to all methods and attributes
    3. Run doctests - all must succeed

Polish:
    1. Zmodyfikuj klasę abstrakcyjną `IrisAbstract`
    2. Dodaj anotację typów do wszystkich metod i atrybutów
    3. Uruchom doctesty - wszystkie muszą się powieść

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

    >>> assert isclass(IrisAbstract)
    >>> assert isabstract(IrisAbstract)
    >>> assert hasattr(IrisAbstract, '__init__')
    >>> assert hasattr(IrisAbstract, 'mean')
    >>> assert hasattr(IrisAbstract, 'sum')
    >>> assert hasattr(IrisAbstract, 'len')
    >>> assert hasattr(IrisAbstract.__init__, '__isabstractmethod__')
    >>> assert hasattr(IrisAbstract.mean, '__isabstractmethod__')
    >>> assert hasattr(IrisAbstract.sum, '__isabstractmethod__')
    >>> assert hasattr(IrisAbstract.len, '__isabstractmethod__')
    >>> assert IrisAbstract.__init__.__isabstractmethod__ == True
    >>> assert IrisAbstract.mean.__isabstractmethod__ == True
    >>> assert IrisAbstract.sum.__isabstractmethod__ == True
    >>> assert IrisAbstract.len.__isabstractmethod__ == True

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

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

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

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

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

from abc import ABC, abstractmethod


class IrisAbstract(ABC):

    @abstractmethod
    def __init__(self, sepal_length, sepal_width, petal_length, petal_width):
        ...

    @abstractmethod
    def mean(self):
        ...

    @abstractmethod
    def sum(self):
        ...

    @abstractmethod
    def len(self):
        ...

# Modify abstract class `IrisAbstract`
# Add type annotation to all methods and attributes

Code 5.29. Solution
"""
* Assignment: OOP AbstractClass Implement
* Complexity: easy
* Lines of code: 5 lines
* Time: 5 min

English:
    1. Define class `Setosa` implementing `IrisAbstract`
    2. All method signatures must be identical to `IrisAbstract`
    3. Don't implement methods, leave `...` or `pass` as content
    4. Run doctests - all must succeed

Polish:
    1. Zdefiniuj klasę `Setosa` implementującą `IrisAbstract`
    2. Sygnatury wszystkich metod muszą być identyczne do `IrisAbstract`
    3. Nie implementuj metod, pozostaw `...` or `pass` jako zawartość
    4. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> import sys; sys.tracebacklimit = 0
    >>> from inspect import isabstract, isclass, ismethod, signature

    >>> assert isclass(IrisAbstract)
    >>> assert isabstract(IrisAbstract)
    >>> assert hasattr(IrisAbstract, '__init__')
    >>> assert hasattr(IrisAbstract, 'mean')
    >>> assert hasattr(IrisAbstract, 'sum')
    >>> assert hasattr(IrisAbstract, 'len')
    >>> assert hasattr(IrisAbstract.__init__, '__isabstractmethod__')
    >>> assert hasattr(IrisAbstract.mean, '__isabstractmethod__')
    >>> assert hasattr(IrisAbstract.sum, '__isabstractmethod__')
    >>> assert hasattr(IrisAbstract.len, '__isabstractmethod__')
    >>> assert IrisAbstract.__init__.__isabstractmethod__ == True
    >>> assert IrisAbstract.mean.__isabstractmethod__ == True
    >>> assert IrisAbstract.sum.__isabstractmethod__ == True
    >>> assert IrisAbstract.len.__isabstractmethod__ == True

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

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

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

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

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

    >>> assert isclass(Setosa)
    >>> result = Setosa(5.1, 3.5, 1.4, 0.2)

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

    >>> assert hasattr(result, '__init__')
    >>> assert hasattr(result, 'len')
    >>> assert hasattr(result, 'sum')
    >>> assert hasattr(result, 'mean')

    >>> assert ismethod(result.__init__)
    >>> assert ismethod(result.len)
    >>> assert ismethod(result.sum)
    >>> assert ismethod(result.mean)

    >>> signature(result.__init__)  # doctest: +NORMALIZE_WHITESPACE
    <Signature (sepal_length: float, sepal_width: float, petal_length:
    float, petal_width: float) -> None>
    >>> signature(result.len)
    <Signature () -> int>
    >>> signature(result.sum)
    <Signature () -> float>
    >>> signature(result.mean)
    <Signature () -> float>

    >>> assert vars(result) == {}, 'Do not implement __init__() method'
    >>> assert result.len() is None, 'Do not implement len() method'
    >>> assert result.mean() is None, 'Do not implement mean() method'
    >>> assert result.sum() is None, 'Do not implement sum() method'
"""

from abc import ABC, abstractmethod


class IrisAbstract(ABC):
    sepal_length: float
    sepal_width: float
    petal_length: float
    petal_width: float

    @abstractmethod
    def __init__(self,
                 sepal_length: float,
                 sepal_width: float,
                 petal_length: float,
                 petal_width: float) -> None:
        ...

    @abstractmethod
    def mean(self) -> float:
        ...

    @abstractmethod
    def sum(self) -> float:
        ...

    @abstractmethod
    def len(self) -> int:
        ...

# Define class `Setosa` implementing `IrisAbstract`
# Don't implement methods, leave `...` or `pass` as content