5.15. 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.15.1. Syntax¶
New class
ABC
hasABCMeta
as its meta classUsing
ABC
as a base class has essentially the same effect as specifyingmetaclass=abc.ABCMeta
, but is simpler to type and easier to readabc.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
5.15.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.15.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
5.15.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' (frozen)>
>>>
>>> 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.15.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.15.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.15.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.15.8. Further Reading¶
5.15.9. References¶
5.15.10. Assignments¶
"""
* 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
"""
"""
* 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()`
"""
* 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
"""
* 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