6.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 classes can have regular methods which will be inherited normally. Some methods can be marked as abstract, and those has to be overwritten in subclasses.
- abstract method¶
Method which has to be present (implemented) in a subclass.
- abstract static method¶
Static method which must be implemented in a subclass.
- abstract property¶
Class variable which has to be present in a subclass.
6.15.1. SetUp¶
>>> from abc import ABC, ABCMeta, abstractmethod, abstractproperty
6.15.2. Syntax¶
Inherit from
ABC
At least one method must be
abstractmethod
orabstractproperty
Body of the method is not important, it could be
raise NotImplementedError
orpass
>>> class Account(ABC):
... @abstractmethod
... def login(self, username: str, password: str) -> None:
... raise NotImplementedError
You cannot create instance of a class Account
as of
this is the abstract class:
>>> mark = Account()
Traceback (most recent call last):
TypeError: Can't instantiate abstract class Account with abstract method login
6.15.3. Implement Abstract Methods¶
All abstract methods must be covered
Abstract base class can have regular (not abstract) methods
Regular methods will be inherited as normal
Regular methods does not need to be overwritten
Abstract base class:
>>> class Account(ABC):
... @abstractmethod
... def login(self, username: str, password: str) -> None:
... raise NotImplementedError
...
... @abstractmethod
... def logout(self) -> None:
... raise NotImplementedError
...
... def say_hello(self):
... return 'hello'
Implementation:
>>> class User(Account):
... def login(self, username: str, password: str) -> None:
... print('Logging-in')
...
... def logout(self) -> None:
... print('Logging-out')
Use:
>>> mark = User()
>>>
>>> mark.login(username='mwatney', password='Ares3')
Logging-in
>>>
>>> mark.logout()
Logging-out
>>>
>>> mark.say_hello()
'hello'
Mind, that all abstract methods must be covered, otherwise it will raise an error. Regular methods (non-abstract) will be inherited as normal and they does not need to be overwritten in an implementing class.
6.15.4. ABCMeta¶
Uses
metaclass=ABCMeta
Not recommended since Python 3.4
Use inheriting
ABC
instead
There is also an alternative (older) way of defining abstract base classes.
It uses metaclass=ABCMeta
specification during class creation.
This method is not recommended since Python 3.4 when ABC
class was
introduce to simplify the process.
>>> class Account(metaclass=ABCMeta):
... @abstractmethod
... def login(self, username: str, password: str) -> None:
... raise NotImplementedError
6.15.5. Abstract Property¶
abc.abstractproperty
is deprecated since Python 3.3Use
property
withabc.abstractmethod
instead
>>> class Account(ABC):
... @abstractproperty
... def AGE_MIN(self) -> int:
... raise NotImplementedError
...
... @abstractproperty
... def AGE_MAX(self) -> int:
... raise NotImplementedError
>>>
>>>
>>> class User(Account):
... AGE_MIN: int = 18
... AGE_MAX: int = 65
Since 3.3 instead of @abstractproperty
using both @property
and @abstractmethod
is recommended.
>>> class Account(ABC):
... @property
... @abstractmethod
... def AGE_MIN(self) -> int:
... raise NotImplementedError
...
... @property
... @abstractmethod
... def AGE_MAX(self) -> int:
... raise NotImplementedError
>>>
>>>
>>> class User(Account):
... AGE_MIN: int = 18
... AGE_MAX: int = 65
Mind that the order here is important and it cannot be the other way around.
Decorator closest to the method must be @abstractmethod
and then
@property
at the most outer level. This is because @abstractmethod
sets special attribute on the method and then this method with attribute
is turned to the property. This does not work if you reverse the order.
6.15.6. Problem: Base Class Has No Abstract Method¶
In order to use Abstract Base Class you must create at least one abstract method. Otherwise it won't prevent from instantiating:
>>> class Account(ABC):
... pass
>>>
>>>
>>> mark = Account()
>>> mark
<__main__.Account object at 0x...>
The code above will allo to create mark
from Account
because
this class did not have any abstract methods.
6.15.7. Problem: Base Class Does Not Inherit From ABC¶
In order to use Abstract Base Class you must inherit from ABC
in your
base class. Otherwise it won't prevent from instantiating:
>>> class Account:
... @abstractmethod
... def login(self, username: str, password: str) -> None:
... raise NotImplementedError
>>>
>>>
>>> class User(Account):
... pass
>>>
>>>
>>> mark = User()
>>> mark
<__main__.User object at 0x...>
This code above will allow to create mark
from User
because
Account
class does not inherit from ABC
.
6.15.8. Problem: All Abstract Methods are not Implemented¶
Must implement all abstract methods:
>>> class Account(ABC):
... @abstractmethod
... def login(self, username: str, password: str) -> None:
... raise NotImplementedError
...
... @abstractmethod
... def logout(self) -> None:
... raise NotImplementedError
>>>
>>>
>>> class User(Account):
... pass
>>>
>>>
>>> mark = User()
Traceback (most recent call last):
TypeError: Can't instantiate abstract class User with abstract methods login, logout
The code above will prevent from creating User
instance,
because class User
does not overwrite all abstract methods.
In fact it does not overwrite any abstract method at all.
6.15.9. Problem: Some Abstract Methods are not Implemented¶
All abstract methods must be implemented in child class:
>>> class Account(ABC):
... @abstractmethod
... def login(self, username: str, password: str) -> None:
... raise NotImplementedError
...
... @abstractmethod
... def logout(self) -> None:
... raise NotImplementedError
>>>
>>>
>>> class User(Account):
... def login(self, username: str, password: str) -> None:
... print('Logging-in')
>>>
>>>
>>> mark = User()
Traceback (most recent call last):
TypeError: Can't instantiate abstract class User with abstract method logout
The code above will prevent from creating User
instance, because class
User
does not overwrite all abstract methods. The .login()
method
is not overwritten. In order abstract class to work, all methods must be
covered.
6.15.10. Problem: Child Class has no Abstract Property¶
Using
abstractproperty
>>> class Account(ABC):
... @abstractproperty
... def AGE_MIN(self) -> int:
... raise NotImplementedError
...
... @abstractproperty
... def AGE_MAX(self) -> int:
... raise NotImplementedError
>>>
>>>
>>> class User(Account):
... AGE_MIN: int = 18
>>>
>>>
>>> mark = User()
Traceback (most recent call last):
TypeError: Can't instantiate abstract class User with abstract method AGE_MAX
The code above will prevent from creating User
instance, because class
User
does not overwrite all abstract properties. The AGE_MAX
is
not covered.
6.15.11. Problem: Child Class has no Abstract Properties¶
Using
property
andabstractmethod
>>> class Account(ABC):
... @property
... @abstractmethod
... def AGE_MIN(self) -> int:
... raise NotImplementedError
...
... @property
... @abstractmethod
... def AGE_MAX(self) -> int:
... raise NotImplementedError
>>>
>>>
>>> class User(Account):
... AGE_MIN: int = 18
>>>
>>>
>>> mark = User()
Traceback (most recent call last):
TypeError: Can't instantiate abstract class User with abstract method AGE_MAX
The code above will prevent from creating User
instance, because class
User
does not overwrite all abstract properties. The AGE_MAX
is
not covered.
6.15.12. Problem: Invalid Order of Decorators¶
Invalid order of decorators:
@property
and@abstractmethod
Should be: first
@property
then@abstractmethod
>>> class Account(ABC):
... @abstractmethod
... @property
... def AGE_MIN(self) -> int:
... raise NotImplementedError
...
... @abstractmethod
... @property
... def AGE_MAX(self) -> int:
... raise NotImplementedError
...
Traceback (most recent call last):
AttributeError: attribute '__isabstractmethod__' of 'property' objects is not writable
Note, that this will not even allow to define User
class at all.
6.15.13. Problem: Overwrite ABC File¶
abc
is common name and it is very easy to create file, variable
or module with the same name as the library, hence overwriting 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.3.app/Contents/plugins/python/helpers/pydev',
'/Applications/PyCharm 2022.3.app/Contents/plugins/python/helpers/pycharm_display',
'/Applications/PyCharm 2022.3.app/Contents/plugins/python/helpers/third_party/thriftpy',
'/Applications/PyCharm 2022.3.app/Contents/plugins/python/helpers/pydev',
'/Applications/PyCharm 2022.3.app/Contents/plugins/python/helpers/pycharm_matplotlib_backend',
'/usr/local/Cellar/python@3.11/3.11.1/Frameworks/Python.framework/Versions/3.11/lib/python311.zip',
'/usr/local/Cellar/python@3.11/3.11.1/Frameworks/Python.framework/Versions/3.11/lib/python3.11',
'/usr/local/Cellar/python@3.11/3.11.1/Frameworks/Python.framework/Versions/3.11/lib/python3.11/lib-dynload',
'/Users/watney/myproject/venv-3.11/lib/python3.11/site-packages']
6.15.14. 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):
... print('Display file content as PDF Document')
>>>
>>> class WordDocument(Document):
... def display(self):
... print('Display file content as Word Document')
>>> file = PDFDocument('myfile.pdf')
>>> file.display()
Display file content as PDF Document
>>> file = WordDocument('myfile.pdf')
>>> file.display()
Display file content as Word Document
>>> file = Document('myfile.txt')
Traceback (most recent call last):
TypeError: Can't instantiate abstract class Document with abstract method display
6.15.15. Use Case - 0x02¶
>>> from abc import ABC, abstractmethod
>>> class UIElement(ABC):
... def __init__(self, name):
... self.name = name
...
... @abstractmethod
... def render(self):
... pass
>>>
>>>
>>> def render(component: list[UIElement]):
... for element in component:
... element.render()
>>> class TextInput(UIElement):
... def render(self):
... print(f'Rendering {self.name} TextInput')
>>>
>>>
>>> class Button(UIElement):
... def render(self):
... print(f'Rendering {self.name} Button')
>>> login_window = [
... TextInput(name='Username'),
... TextInput(name='Password'),
... Button(name='Submit'),
... ]
>>>
>>> render(login_window)
Rendering Username TextInput
Rendering Password TextInput
Rendering Submit Button
6.15.16. 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)
6.15.17. Further Reading¶
6.15.18. References¶
6.15.19. 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