3.5. 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
3.5.1. Syntax¶
New class
ABC
hasABCMeta
as its meta class.Using
ABC
as a base class has essentially the same effect as specifyingmetaclass=abc.ABCMeta
, but is simpler to type and easier to read.abc.ABC
basically just an extra layer overmetaclass=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.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
>>> 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.3. Abstract Property¶
abc.abstractproperty
is deprecated since Python 3.3Use
property
withabc.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.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)
['/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.5. 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.6. Further Reading¶
3.5.7. References¶
3.5.8. Assignments¶
"""
* 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
"""
"""
* 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
"""
* 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