3.5. OOP Abstract Class

3.5.1. Rationale

  • 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

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

3.5.3. 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
>>> from abc import ABCMeta, abstractmethod
>>>
>>>
>>> class Astronaut(metaclass=ABCMeta):
...     @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

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

3.5.5. 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)  
['/Applications/PyCharm 2021.1 EAP.app/Contents/plugins/python/helpers/pydev',
 '/Users/watney/book-python',
 '/Applications/PyCharm 2021.3 EAP.app/Contents/plugins/python/helpers/pycharm_display',
 '/Applications/PyCharm 2021.3 EAP.app/Contents/plugins/python/helpers/third_party/thriftpy',
 '/Applications/PyCharm 2021.3 EAP.app/Contents/plugins/python/helpers/pydev',
 '/usr/local/Cellar/python@3.10/3.10.0/Frameworks/Python.framework/Versions/3.10/lib/python310.zip',
 '/usr/local/Cellar/python@3.10/3.10.0/Frameworks/Python.framework/Versions/3.10/lib/python3.10',
 '/usr/local/Cellar/python@3.10/3.10.0/Frameworks/Python.framework/Versions/3.10/lib/python3.10/lib-dynload',
 '/Users/watney/.virtualenvs/python-3.10/lib/python3.10/site-packages',
 '/Applications/PyCharm 2021.3 EAP.app/Contents/plugins/python/helpers/pycharm_matplotlib_backend',
 '/Users/watney/book-python',
 '/Users/watney/book-python/_tmp']

3.5.6. Use Cases

Abstract Class:

>>> from abc import ABC, abstractmethod
>>>
>>>
>>> class Document(ABC):
...     def __init__(self, filename):
...         self.filename = filename
...         # with open(filename, mode='rb') as file:
...         #     self.content = file.read()
...
...     @abstractmethod
...     def display(self):
...         pass
>>>
>>>
>>> class PDFDocument(Document):
...     def display(self):
...         """display self.content as PDF Document"""
>>>
>>> class WordDocument(Document):
...     def display(self):
...         """display self.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

3.5.7. Further Reading

3.5.8. References

1

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

3.5.9. Assignments

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

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

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

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

    >>> assert isabstract(Iris), \
    'Iris class should be abstract, inherit from ABC or use metaclass=ABCMeta'

    >>> assert hasattr(Iris.get_name, '__isabstractmethod__'), \
    'Iris.get_name() method should be abstract, use @abstractmethod decorator'

    >>> assert not isabstract(Setosa), \
    'Setosa should not be abstract class'

    >>> assert not hasattr(Setosa.get_name, '__isabstractmethod__'), \
    'Setosa.get_name() should not be an abstract method'

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

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 there is "method" word in doctest
    * So it differs by "s" at the end of "method" word
"""

Code 3.20. Solution
"""
* Assignment: OOP Abstract Interface
* Complexity: easy
* Lines of code: 10 lines
* Time: 5 min

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

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

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

    >>> assert isabstract(IrisAbstract), \
    'IrisAbstract should be an abstract class, inherit from ABC or use ABCMeta'

    >>> assert hasattr(IrisAbstract, 'mean'), \
    'IrisAbstract, should have .mean() abstract method'

    >>> assert hasattr(IrisAbstract, 'sum'), \
    'IrisAbstract should have .sum() abstract method'

    >>> assert hasattr(IrisAbstract, 'len'), \
    'IrisAbstract should have .len() abstract method'

    >>> assert hasattr(IrisAbstract.mean, '__isabstractmethod__'), \
    'IrisAbstract.mean() should be an abstract method, use @abstractmethod'

    >>> assert hasattr(IrisAbstract.sum, '__isabstractmethod__'), \
    'IrisAbstract.sum() should be an abstract method, use @abstractmethod'

    >>> assert hasattr(IrisAbstract.len, '__isabstractmethod__'), \
    'IrisAbstract.len() should be an abstract method, use @abstractmethod'
"""

from abc import ABCMeta, abstractmethod


Code 3.21. Solution
"""
* Assignment: OOP Abstract Annotate
* Complexity: easy
* Lines of code: 13 lines
* Time: 8 min

English:
    1. Define abstract class `IrisAbstract`
    2. Attributes: `sepal_length, sepal_width, petal_length, petal_width`
    3. Abstract methods: `__init__`, `sum()`, `len()`, `mean()`
    4. Add type annotation to all methods and attibutes
    5. Run doctests - all must succeed

Polish:
    1. Zdefiniuj klasę abstrakcyjną `IrisAbstract`
    2. Atrybuty: `sepal_length, sepal_width, petal_length, petal_width`
    3. Metody abstrakcyjne: `__init__`, `sum()`, `len()`, `mean()`
    4. Dodaj anotację typów do wszystkich metod i atrybutów
    5. Uruchom doctesty - wszystkie muszą się powieść

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

    >>> assert isabstract(IrisAbstract), \
    'IrisAbstract should be an abstract class, inherit from ABC or use ABCMeta'

    >>> assert hasattr(IrisAbstract, '__init__'), \
    'IrisAbstract, should have .__init__() abstract method'

    >>> assert hasattr(IrisAbstract, 'mean'), \
    'IrisAbstract, should have .mean() abstract method'

    >>> assert hasattr(IrisAbstract, 'sum'), \
    'IrisAbstract should have .sum() abstract method'

    >>> assert hasattr(IrisAbstract, 'len'), \
    'IrisAbstract should have .len() abstract method'

    >>> assert hasattr(IrisAbstract.__init__, '__isabstractmethod__'), \
    'IrisAbstract.__init__() should be an abstract method, use @abstractmethod'

    >>> assert hasattr(IrisAbstract.mean, '__isabstractmethod__'), \
    'IrisAbstract.mean() should be an abstract method, use @abstractmethod'

    >>> assert hasattr(IrisAbstract.sum, '__isabstractmethod__'), \
    'IrisAbstract.sum() should be an abstract method, use @abstractmethod'

    >>> assert hasattr(IrisAbstract.len, '__isabstractmethod__'), \
    'IrisAbstract.len() should be an abstract method, use @abstractmethod'

    >>> assert hasattr(IrisAbstract, '__annotations__'), \
    'IrisAbstract class should have fields type annotations'

    >>> assert hasattr(IrisAbstract.__init__, '__annotations__'), \
    'IrisAbstract.__init__() method should have parameter type annotations'

    >>> 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 ABCMeta, abstractmethod