10.1. 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.
10.1.1. SetUp
>>> from abc import ABC, ABCMeta, abstractmethod, abstractproperty
10.1.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) -> 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 without an implementation for abstract method 'login'
10.1.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):
... def __init__(self, username: str, password: str) -> None:
... self.username = username
... self.password = password
...
... @abstractmethod
... def login(self) -> None:
... raise NotImplementedError
...
... @abstractmethod
... def logout(self) -> None:
... raise NotImplementedError
Implementation:
>>> class User(Account):
... def login(self) -> None:
... print('Logging-in')
...
... def logout(self) -> None:
... print('Logging-out')
Use:
>>> mark = User(username='mwatney', password='Ares3')
>>>
>>> mark.login()
Logging-in
>>>
>>> mark.logout()
Logging-out
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.
10.1.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):
... def __init__(self, username: str, password: str) -> None:
... self.username = username
... self.password = password
...
... @abstractmethod
... def login(self) -> None:
... raise NotImplementedError
10.1.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.
10.1.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.
10.1.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) -> 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
.
10.1.8. Problem: All Abstract Methods are not Implemented
Must implement all abstract methods:
>>> class Account(ABC):
... @abstractmethod
... def login(self) -> 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 without an implementation for 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.
10.1.9. Problem: Some Abstract Methods are not Implemented
All abstract methods must be implemented in child class:
>>> class Account(ABC):
... @abstractmethod
... def login(self) -> None:
... raise NotImplementedError
...
... @abstractmethod
... def logout(self) -> None:
... raise NotImplementedError
>>>
>>>
>>> class User(Account):
... def login(self) -> None:
... print('Logging-in')
>>>
>>>
>>> mark = User()
Traceback (most recent call last):
TypeError: Can't instantiate abstract class User without an implementation for 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.
10.1.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 without an implementation for 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.
10.1.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 without an implementation for 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.
10.1.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.
10.1.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']
10.1.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 without an implementation for abstract method 'display'
10.1.15. Use Case - 0x02
>>> from abc import ABC, abstractmethod
>>> class Element(ABC):
... def __init__(self, name):
... self.name = name
...
... @abstractmethod
... def render(self):
... pass
>>>
>>>
>>> def render(component: list[Element]):
... for element in component:
... element.render()
>>> class TextInput(Element):
... def render(self):
... print(f'Rendering {self.name} TextInput')
>>>
>>>
>>> class Button(Element):
... 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
10.1.16. Use Case - 0x03
>>> from abc import ABC, abstractmethod, abstractproperty
>>>
>>> class Account(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 User(Account):
... AGE_MIN = 30
... AGE_MAX = 50
>>> mark = User(age=42)
10.1.17. Use Case - 0x04
>>> from abc import ABC, abstractmethod
>>> from datetime import timedelta
>>>
>>>
>>> class Cache(ABC):
... @property
... @abstractmethod
... def timeout(self) -> timedelta:
... raise NotImplementedError
...
... @abstractmethod
... def set(self, key: str, value: str) -> None:
... raise NotImplementedError
...
... @abstractmethod
... def get(self, key: str) -> str:
... raise NotImplementedError
...
... @abstractmethod
... def delete(self, key: str) -> None:
... raise NotImplementedError
>>>
>>>
>>> class DatabaseCache(Cache):
... timeout = timedelta(minutes=10)
... def set(self, key: str, value: str) -> None: ...
... def get(self, key: str) -> str: ...
... def delete(self, key: str) -> None: ...
>>>
>>> class FilesystemCache(Cache):
... timeout = timedelta(minutes=10)
... def set(self, key: str, value: str) -> None: ...
... def get(self, key: str) -> str: ...
... def delete(self, key: str) -> None: ...
>>>
>>> class LocmemCache(Cache):
... timeout = timedelta(minutes=10)
... def set(self, key: str, value: str) -> None: ...
... def get(self, key: str) -> str: ...
... def delete(self, key: str) -> None: ...
>>>
>>>
>>> cache: Cache = LocmemCache()
>>> cache.set('firstname', 'Mark')
>>> cache.set('lastname', 'Watney')
>>> cache.get('firstname')
>>> cache.get('lastname')
>>> cache.delete('firstname')
>>> cache.delete('lastname')
10.1.18. Further Reading
10.1.19. References
10.1.20. Assignments
"""
* Assignment: OOP AbstractClass Syntax
* Complexity: easy
* Lines of code: 8 lines
* Time: 3 min
English:
1. Create polymorphism class `Account`
2. Define polymorphism methods `login()` and `logout()`
3. Run doctests - all must succeed
Polish:
1. Stwórz klasę abstrakcyjną `Account`
2. Zdefiniuj metody abstrakcyjne `login()` i `logout()`
3. Uruchom doctesty - wszystkie muszą się powieść
Tests:
>>> import sys; sys.tracebacklimit = 0
>>> from inspect import isclass, isabstract, ismethod
>>> assert isclass(Account)
>>> assert isabstract(Account)
>>> assert hasattr(Account, 'login')
>>> assert hasattr(Account, 'logout')
>>> assert hasattr(Account.login, '__isabstractmethod__')
>>> assert hasattr(Account.logout, '__isabstractmethod__')
>>> assert Account.login.__isabstractmethod__ == True
>>> assert Account.logout.__isabstractmethod__ == True
>>> result = Account()
Traceback (most recent call last):
TypeError: Can't instantiate abstract class Account without an implementation for abstract methods 'login', 'logout'
"""
# Define polymorphism class `Account`
# With polymorphism methods `login()` and `logout()`
class Account:
pass
"""
* Assignment: OOP AbstractClass Implementation
* Complexity: easy
* Lines of code: 5 lines
* Time: 3 min
English:
1. Create class `User` inheriting from `Account`
2. Overwrite all polymorphism methods, leave `pass` as content
3. Run doctests - all must succeed
Polish:
1. Stwórz klasę `User` dziedziczące po `Account`
2. Nadpisz wszystkie metody abstrakcyjne, pozostaw `pass` jako treść
3. Uruchom doctesty - wszystkie muszą się powieść
Tests:
>>> import sys; sys.tracebacklimit = 0
>>> from inspect import isclass, isabstract, ismethod
>>> assert isclass(Account)
>>> assert isclass(User)
>>> assert isabstract(Account)
>>> assert not isabstract(User)
>>> assert hasattr(Account, 'login')
>>> assert hasattr(Account, 'logout')
>>> assert hasattr(User, 'login')
>>> assert hasattr(User, 'logout')
>>> assert not hasattr(User.login, '__isabstractmethod__')
>>> assert not hasattr(User.logout, '__isabstractmethod__')
>>> assert hasattr(Account.login, '__isabstractmethod__')
>>> assert hasattr(Account.logout, '__isabstractmethod__')
>>> assert Account.login.__isabstractmethod__ == True
>>> assert Account.logout.__isabstractmethod__ == True
>>> result = Account()
Traceback (most recent call last):
TypeError: Can't instantiate abstract class Account without an implementation for abstract methods 'login', 'logout'
>>> result = User()
>>> assert ismethod(result.login)
"""
from abc import ABC, abstractmethod
class Account(ABC):
@abstractmethod
def login(self):
pass
@abstractmethod
def logout(self):
pass
# Create class `User` inheriting from `Account`
# Overwrite all polymorphism methods, leave `pass` as content
class User:
...
"""
* Assignment: OOP AbstractClass Typing
* Complexity: easy
* Lines of code: 5 lines
* Time: 3 min
English:
1. Modify polymorphism class `Account`
2. Add type annotation to all methods and attributes
3. Run doctests - all must succeed
Polish:
1. Zmodyfikuj klasę abstrakcyjną `Account`
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, ismethod, signature
>>> assert isclass(Account)
>>> assert isabstract(Account)
>>> assert hasattr(Account, '__init__')
>>> assert hasattr(Account, 'login')
>>> assert hasattr(Account, 'logout')
>>> assert hasattr(Account.__init__, '__isabstractmethod__')
>>> assert hasattr(Account.login, '__isabstractmethod__')
>>> assert hasattr(Account.logout, '__isabstractmethod__')
>>> assert Account.__init__.__isabstractmethod__ == True
>>> assert Account.login.__isabstractmethod__ == True
>>> assert Account.logout.__isabstractmethod__ == True
>>> Account.__annotations__
{'username': <class 'str'>, 'password': <class 'str'>}
>>> Account.__init__.__annotations__
{'username': <class 'str'>, 'password': <class 'str'>, 'return': None}
>>> Account.login.__annotations__
{'return': None}
>>> Account.logout.__annotations__
{'return': None}
"""
from abc import ABC, abstractmethod
# Modify class to add type annotation to all methods and attributes
class Account(ABC):
username: str
password: str
@abstractmethod
def __init__(self, username, password):
...
@abstractmethod
def login(self):
...
@abstractmethod
def logout(self):
...
"""
* Assignment: OOP AbstractClass Implement
* Complexity: easy
* Lines of code: 5 lines
* Time: 3 min
English:
1. Define class `User` implementing `Account`
2. Overwrite all polymorphism methods, leave `pass` as content
3. Run doctests - all must succeed
Polish:
1. Zdefiniuj klasę `User` implementującą `Account`
2. Nadpisz wszystkie metody abstrakcyjne, pozostaw `pass` jako treść
3. Uruchom doctesty - wszystkie muszą się powieść
Tests:
>>> import sys; sys.tracebacklimit = 0
>>> from inspect import isabstract, isclass, ismethod, signature
>>> assert isclass(Account)
>>> assert isabstract(Account)
>>> assert hasattr(Account, '__init__')
>>> assert hasattr(Account, 'login')
>>> assert hasattr(Account, 'logout')
>>> assert hasattr(Account.__init__, '__isabstractmethod__')
>>> assert hasattr(Account.login, '__isabstractmethod__')
>>> assert hasattr(Account.logout, '__isabstractmethod__')
>>> assert Account.__init__.__isabstractmethod__ == True
>>> assert Account.login.__isabstractmethod__ == True
>>> assert Account.logout.__isabstractmethod__ == True
>>> Account.__annotations__
{'username': <class 'str'>, 'password': <class 'str'>}
>>> Account.__init__.__annotations__
{'username': <class 'str'>, 'password': <class 'str'>, 'return': None}
>>> Account.login.__annotations__
{'return': None}
>>> Account.logout.__annotations__
{'return': None}
>>> assert isclass(User)
>>> result = User(username='mwatney', password='Ares3')
>>> result.__annotations__
{'username': <class 'str'>, 'password': <class 'str'>}
>>> assert hasattr(result, '__init__')
>>> assert hasattr(result, 'logout')
>>> assert hasattr(result, 'login')
>>> assert ismethod(result.__init__)
>>> assert ismethod(result.logout)
>>> assert ismethod(result.login)
>>> signature(result.__init__) # doctest: +NORMALIZE_WHITESPACE
<Signature (username: str, password: str) -> None>
>>> signature(result.logout)
<Signature () -> None>
>>> signature(result.login)
<Signature () -> None>
>>> assert vars(result) == {}, 'Do not implement __init__() method'
>>> assert result.login() is None, 'Do not implement login() method'
>>> assert result.logout() is None, 'Do not implement logout() method'
"""
from abc import ABC, abstractmethod
class Account(ABC):
username: str
password: str
@abstractmethod
def __init__(self, username: str, password: str) -> None:
...
@abstractmethod
def login(self) -> None:
...
@abstractmethod
def logout(self) -> None:
...
# Define class `User` implementing `Account`
# Overwrite all polymorphism methods, leave `pass` as content
class User:
...