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

from abc import ABCMeta, abstractproperty


class HasGold(metaclass=ABCMeta):
    @abstractproperty
    def GOLD_MIN(self):
        raise NotImplementedError

    @abstractproperty
    def GOLD_MAX(self):
        raise NotImplementedError


class Hero(HasGold):
    GOLD_MIN: int = 1
    GOLD_MAX: int = 10
    name: str

    def __init__(self, name):
        self.name = name


my = Hero('Mark Watney')
print(my.name)
# Mark Watney
from abc import ABCMeta, abstractproperty


class HasGold(metaclass=ABCMeta):
    @abstractproperty
    def GOLD_MIN(self):
        raise NotImplementedError

    @abstractproperty
    def GOLD_MAX(self):
        raise NotImplementedError


class Hero(HasGold):
    name: str

    def __init__(self, name):
        self.name = name


my = Hero('Mark Watney')
print(my.name)
# Traceback (most recent call last):
# TypeError: Can't instantiate abstract class Hero with abstract methods GOLD_MAX, GOLD_MIN

6.10.5. Errors

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()
print('no errors')
# no errors

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()
print('no errors')
# no errors

Must implement all abstract methods:

from abc import ABCMeta, abstractmethod


class Human(metaclass=ABCMeta):
    @abstractmethod
    def say_hello(self):
        pass


class Astronaut(Human):
    pass


astro = Astronaut()
# Traceback (most recent call last):
# TypeError: Can't instantiate abstract class Astronaut with abstract method say_hello

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

Code 6.38. 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.39. Solution
"""
* Assignment: OOP Abstract Interface
* 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. 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. 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__
"""


# Given
from abc import ABCMeta, abstractmethod