Interfejsy są potężnym narzędziem w programowaniu obiektowym. Wymuszają zgodność - klasa implementująca interfejs musi mieć zadeklarowane metody przez tenże interfejs wymagane. W językach kompilowanych, czy też nawet w typescriptcie, program zwyczajnie się nie skompiluje jeśli to wymaganie nie będzie spełnione. Jest to wspaniałe zabezpieczenie przed pominięciem implementacji - kod padnie jeszcze nim opuści maszyne developera.
W Pythonie stworzenie interfejsu który spowoduje błąd w momencie "kompilacji" jest niemożliwe - po prostu nie ma kompilacji. Python jest językiem interpretowanym i błąd może się pojawić w dwóch momentach:
- przy próbie wykonania nieistniejącej metody
- przy inicjalizacji nowego obiektu danej klasy
Interfejsy możemy stworzyć na kilka sposobów:
- Interfejs ze zwracaniem wyjątków.
- Interfejs z kontrolą implementacji w konstruktorze
- Interfejs z wykorzystaniem abc.ABCMeta
Interfejs ze zwracaniem wyjątków
To najprostsza wersja. Popatrzmy na poniższy kod:
from dataclasses import dataclass
@dataclass
class Citizen:
age: int = 0
def is_mature(self) -> bool:
raise NotImplementedError
class UsCitizen(Citizen):
maturity_limit = 21
class PolishCitizen(Citizen):
maturity_limit = 18
if __name__ == "__main__":
pole = PolishCitizen(age=16)
print(pole)
» python3 citizen.py
PolishCitizen(age=16)
Wszystko fajnie, kod się uruchamia. Problem w tym, że nasz Polak nie ma zaimplentowanej metody is_mature
. Błąd pojawi się dopiero w momencie uruchomienia pole.is_mature()
. Trochę to słabe rozwiązanie i może bardzo boleć kiedy taki kod trafi na produkcje.
Kiedy można je stosować? Wcale - jest zbyt niebezpiecznie. Pewną wariacją na jego temat jest kod poniżej — nadaje się on jednak tylko do to drobnej pomocy przy projektowaniu kodu i jest równie niebezpieczny. Jedynym plusem są komentarze TODO, które nasz edytor powinien ładnie podświetlić.
from dataclasses import dataclass
@dataclass
class Citizen:
age: int = 0
def is_mature(self) -> bool:
raise NotImplementedError
class UsCitizen(Citizen):
maturity_limit = 21
def is_mature(self) -> bool:
# TODO implement
raise NotImplementedError
class PolishCitizen(Citizen):
maturity_limit = 18
def is_mature(self) -> bool:
# TODO implement
raise NotImplementedError
if __name__ == "__main__":
pole = PolishCitizen(age=16)
print(pole)
Podsumowując: takie konstrukcje w kodzie to proszenie się o kłopoty.
Interfejs z kontrolą implementacji w konstruktorze
Jak się można zabezpieczyć przed problemem opisanym wyżej? Sprawdzając, czy klasa implementuje wymagane metody! Przeanalizujmy poniższy kod:
from dataclasses import dataclass
@dataclass
class Citizen:
age: int = 0
def __init__(self, *args, **kwargs) -> None:
if not hasattr(self.__class__, "is_mature") or not callable(
self.__class__.is_mature
):
raise NotImplementedError
class UsCitizen(Citizen):
maturity_limit = 21
class PolishCitizen(Citizen):
maturity_limit = 18
if __name__ == "__main__":
pole = PolishCitizen(age=16)
print(pole)
» python3 citizen.py
Traceback (most recent call last):
File "/Users/jacek/Workspace/akademiaIT/blog/citizen.py", line 24, in <module>
pole = PolishCitizen(age=16)
File "/Users/jacek/Workspace/akademiaIT/blog/citizen.py", line 12, in __init__
raise NotImplementedError
NotImplementedError
Co tu się dzieje?
W klasie Citizen, nadrzędnej, dodaliśmy konstruktor, który w momencie inicjalizacji obiektów szuka metody is_mature
i jeśli jej nie ma, zwraca błąd. Co ważne, ta metoda nie może być zadeklarowana w klasie Citizen - inaczej konstruktor ją znajdzie i nici z całego pomysłu.
W tym momencie uruchomienie powyższego pliku zwróci bardzo ładny błąd informujący o brakującej metodzie. Nieźle. Co prawda nie jest to zachowanie znane z C++. Nadal musimy spróbować stworzyć obiekt, by wywołać błąd. Niemniej, stworzenie obiektu jest dużo prostsze od wywoływania konkretnej metody i zwiększa sporo szanse znalezienia defektu, nim kod zostanie przesłany dalej.
Interfejs z wykorzystaniem abc.ABCMeta
Z pomocą przychodzą nam Abstract Base Classes (https://peps.python.org/pep-3119/)
Przeanalizujmy poniższy kod:
import abc
from dataclasses import dataclass
@dataclass
class Citizen(metaclass=abc.ABCMeta):
age: int = 0
@abc.abstractmethod
def is_mature(self) -> bool:
pass
class UsCitizen(Citizen):
maturity_limit = 21
class PolishCitizen(Citizen):
maturity_limit = 18
if __name__ == "__main__":
pole = PolishCitizen(age=16)
print(pole)
Wprowadzone przez nas zmiany nie sa duże. Dodaliśmy metaklasę do Citizen i skorzystaliśmy z dekoratora @abc.abstractmethod przy definicji metody is_mature. Co się stanie jak uruchomimy plik?
>> python3 citizenABC.py
Traceback (most recent call last):
File "/Users/jacek/Workspace/akademiaIT/blog/citizenABC.py", line 23, in <module>
pole = PolishCitizen(age=16)
TypeError: Can't instantiate abstract class PolishCitizen with abstract method is_mature
O jakie ładne. Kod mamy bardzo czytelny, widać co jest czym, dekorator ładnie określa metodę abstrakcyjną. Gratis dostaliśmy czytelny błąd ze wskazaniem miejsca. Kod niestety dalej ma tę wadę, że trzeba spróbować stworzyć obiekt danej klasy, by błąd się pojawił. No cóż, taka już specyfika języka.
Gorąco zachęcam do przejrzenia Abstract Base Classes (https://docs.python.org/3/library/abc.html) - potrafią sporo więcej niż tu pokazałem.
Spodobał Ci się post? Udostępnij go na swoich kanałach!
Masz uwagi do posta, chcesz porozmawiać, szukasz pomocy z Pythonem i Django? Napisz do mnie!