6.10. Abstract Class

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

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

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


class MyClass(metaclass=ABCMeta):

    @abstractmethod
    def mymethod(self):
        pass

6.10.3. Abstract Method

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

6.10.4. Abstract Property

  • abc.abstractproperty is deprecated since Python 3.3

  • Use property with abc.abstractmethod instead

from abc import ABCMeta, abstractproperty


class Monster(metaclass=ABCMeta):
    @abstractproperty
    def DAMAGE(self) -> int:
        pass

class Dragon(Monster):
    DAMAGE: int = 10


d = Dragon()

print('no errors')
# no errors
from abc import ABCMeta, abstractmethod


class Monster(metaclass=ABCMeta):
    @property
    @abstractmethod
    def DAMAGE(self) -> int:
        pass


class Dragon(Monster):
    DAMAGE: int = 10


d = Dragon()

print('no errors')
# no errors

6.10.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 ABCMeta


class Astronaut(metaclass=ABCMeta):
    pass


astro = Astronaut()   # It will not raise an error, because there are no abstractmethods

print('no errors')
# no errors

Must implement all abstract methods:

from abc import ABCMeta, abstractmethod


class Human(metaclass=ABCMeta):
    @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 ABCMeta, abstractmethod


class Human(metaclass=ABCMeta):
    @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 ABCMeta, abstractproperty


class Monster(metaclass=ABCMeta):
    @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 ABCMeta, abstractmethod


class Monster(metaclass=ABCMeta):
    @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 ABCMeta, abstractmethod


class Monster(metaclass=ABCMeta):
    @abstractmethod
    @property
    def DAMAGE(self) -> int:
        pass


class Dragon(Monster):
    DAMAGE: int = 10


d = Dragon()
# Traceback (most recent call last):
# AttributeError: attribute '__isabstractmethod__' of 'property' objects is not writable

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 '/usr/local/Cellar/python@3.9/3.9.0/Frameworks/Python.framework/Versions/3.9/lib/python3.9/abc.py'>

pprint(sys.path)
# ['/Applications/PyCharm 2020.3 EAP.app/Contents/plugins/python/helpers/pydev',
#  '/Users/watney/book-python',
#  '/Applications/PyCharm 2020.3 EAP.app/Contents/plugins/python/helpers/pycharm_display',
#  '/Applications/PyCharm 2020.3 EAP.app/Contents/plugins/python/helpers/third_party/thriftpy',
#  '/Applications/PyCharm 2020.3 EAP.app/Contents/plugins/python/helpers/pydev',
#  '/usr/local/Cellar/python@3.9/3.9.0/Frameworks/Python.framework/Versions/3.9/lib/python39.zip',
#  '/usr/local/Cellar/python@3.9/3.9.0/Frameworks/Python.framework/Versions/3.9/lib/python3.9',
#  '/usr/local/Cellar/python@3.9/3.9.0/Frameworks/Python.framework/Versions/3.9/lib/python3.9/lib-dynload',
#  '/Users/watney/.virtualenvs/python-3.9/lib/python3.9/site-packages',
#  '/Applications/PyCharm 2020.3 EAP.app/Contents/plugins/python/helpers/pycharm_matplotlib_backend',
#  '/Users/watney/book-python',
#  '/Users/watney/book-python/_tmp']

6.10.6. Use Cases

Abstract Class:

from abc import ABC, abstractmethod


class Document(ABC):
    def __init__(self, file):
        with open(file, mode='rb') as file:
            self.file = 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('filename.pdf')
file1.display()

file2 = Document('filename.txt')
# Traceback (most recent call last):
# TypeError: Can't instantiate abstract class Document with abstract method display

6.10.8. Assignments

Code 6.24. 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. Compare result with "Tests" section (see below)

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. Porównaj wyniki z sekcją "Tests" (patrz poniżej)

Tests:
    >>> 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 6.25. Solution
"""
* Assignment: OOP Abstract Interface
* Complexity: easy
* Lines of code: 10 lines
* Time: 8 min

English:
    1. Define abstract class `IrisAbstract`
    3. Abstract methods: `__init__`, `sum()`, `len()`, `mean()`
    4. Compare result with "Tests" section (see below)

Polish:
    1. Zdefiniuj klasę abstrakcyjną `IrisAbstract`
    3. Metody abstrakcyjne: `__init__`, `sum()`, `len()`, `mean()`
    4. Porównaj wyniki z sekcją "Tests" (patrz poniżej)

Tests:
    >>> from inspect import isabstract
    >>> assert isabstract(IrisAbstract)
    >>> assert hasattr(IrisAbstract, 'mean')
    >>> assert hasattr(IrisAbstract, 'sum')
    >>> assert hasattr(IrisAbstract, 'len')
    >>> assert IrisAbstract.mean.__isabstractmethod__
    >>> assert IrisAbstract.sum.__isabstractmethod__
    >>> assert IrisAbstract.len.__isabstractmethod__
"""


# Given
from abc import ABCMeta, abstractmethod


Code 6.26. Solution
"""
* Assignment: OOP Abstract Annotate
* Complexity: easy
* Lines of code: 13 lines
* Time: 13 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. Compare result with "Tests" section (see below)

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. Porównaj wyniki z sekcją "Tests" (patrz poniżej)

Tests:
    >>> from inspect import isabstract
    >>> assert isabstract(IrisAbstract)
    >>> assert hasattr(IrisAbstract, '__init__')
    >>> assert hasattr(IrisAbstract, 'mean')
    >>> assert hasattr(IrisAbstract, 'sum')
    >>> assert hasattr(IrisAbstract, 'len')
    >>> assert IrisAbstract.__init__.__isabstractmethod__
    >>> assert IrisAbstract.mean.__isabstractmethod__
    >>> assert IrisAbstract.sum.__isabstractmethod__
    >>> assert IrisAbstract.len.__isabstractmethod__

    >>> 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'>}
"""


# Given
from abc import ABCMeta, abstractmethod