Podtypowanie strukturalne w Pythonie a klasy abstrakcyjne

Cześć.

Dziś będzie trochę o klasach abstrakcyjnych, interfejsach i czymś, co jest jeszcze dość świeżą sprawą w Pythonie, czyli podtypowaniu strukturalnym czyli protokołach.

Klasa abstrakcyjna

Tutaj sprawa jest prosta. Klasa abstrakcyjna to taka klasa której instancji nie możemy stworzyć. Służy ona do tego by z niej dziedziczyć i implementować te metody które oznaczone zostały jako abstrakcyjne. Przykład takiej klasy:

1
2
3
4
5
6
7
8
9
10
11
12
13
from abc import ABC, abstractmethod


class AbstractClass(ABC):
  @abstractmethod
  def method1(self):
      pass

  def method2(self):
      return "Helo!"


print(AbstractClass())

Uruchomienie tego kodu spowoduje błąd:

TypeError: Can't instantiate abstract class AbstractObject with abstract method method1

Co jest w pełni tym, czego oczekiwaliśmy. Jeśli natomiast zaimplementujemy tę metodę w klasie podrzędnej, to Python już pozwoli nam stworzyć jej instancję:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from abc import ABC, abstractmethod


class AbstractClass(ABC):
  @abstractmethod
  def method1(self):
      pass
 
  def method2(self):
      return "Helo!"

  def __str__(self):
      return f'Jestem {self.__class__.__name__}'


class ConcreteClass(AbstractClass):
  def method1(self):
      print("Heeelo!")


print(ConcreteClass())

I taki kod (dzięki implementacji metody str) wypisze nam „Jestem ConcreteClass”.

Klasa abstrakcyjna służy przede wszystkim jako baza dla klas potomnych.

Interfejs

Interfejs to narzędzie umożliwiająca „wymuszenie” na klasie implementacji pewnych metod ale w odróżnieniu od klasy abstrakcyjnej nie niesie on za sobą żadnych gotowym implementacji. Bardzo, bardzo przydatne narzędzie, które… Nie jest dostępne w Pythonie. Jednak nic straconego. Najpopularniejszym sposobem radzenia sobie z tym są po prostu klasy abstrakcyjne nie posiadająca żadnych zaimplementowanych metod. Czyli np. klasa „CustomObjectInterface” mogłaby spełniać funkcję interfejsu (i nie tylko ze względu na nazwę):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from abc import ABC, abstractmethod


class CustomObjectInterface(ABC):
  @abstractmethod
  def method1(self):
      pass

class CustomObject(CustomObjectInterface):
  def method1(self):
      print("Heeelo!")


print(CustomObject())

ponieważ wszystkie jej metody są abstrakcyjne.

Pułapki

Pierwsza pułapka kryje się w sytuacji kiedy mamy dużą klasę abstrakcyjną. Sprawia to, że jej klasy potomne muszą implementować wszystkie metody abstrakcyjne, nawet jeśli ich nie używają, to produkuje potworki w stylu ogromnej ilości metod z ciałem równym „pass”. A to sprawia, że łamiemy nie jedną aż trzy zasady SOLID: „Interface Segregation Principle”, „Single Resposibility Principle” i „Liskov Substitution Principle”. Pierwsza zasada mówi wprost o rozdzielaniu interfejsów, druga o pojedynczej odpowiedzialności, której w tym przypadku rzecz jasna nie mamy, natomiast trzecia mówi o tym, by wszędzie tam gdzie przekazujemy obiekt klasy bazowej, powinniśmy móc przekazać obiekt klasy dziedziczącej po tej klasie, a tym przypadku, używając słówka „pass” łatwo zmienić typ zwracany na „None” i tym samym stracić kompatybilność Dobra praktyką jest po prostu robienie możliwie najmniejszych interfejsów.

Druga pułapka to pokusa by nasze klasy abstrakcyjne, które na początku odgrywają rolę interfejsów, po pewnym czasie zaczęły posiadać konkretne implementacje, co sprawia, że przestajemy mieć rozdzielenie pomiędzy abstrakcją a implementacją. A jeżeli połączymy to z pierwszą pułapką, to dostaniemy cała rodzinę klas, które posiadają nieużywane metody.


Czekaj, stop!

Podoba Ci się to co tworzę? Jeśli tak to zapraszam Cię do zapisania się na newsletter:

Aby potwierdzić swoją subskrypcję, odbierz pocztę i kliknij w link potwierdzający:) jeśli maila nie ma to poczekaj chwile i/lub sprawdź folder spam/inne/oferty itp :)

Aby potwierdzić swoją subskrypcję, odbierz pocztę i kliknij w link potwierdzający:) jeśli maila nie ma to poczekaj chwile i/lub sprawdź folder spam/inne/oferty itp :)
a w ramach prezentu otrzymasz całkowicie za darmo, dwa dokumenty PDF „6 (nie zawsze oczywistych) błędów popełnianych podczas nauki programowania” który jest jednym z efektów ponad siedmioletniej pracy i obserwacji rozwoju niejednego programisty oraz „Wstęp do testowania w Pythonie”, będący wprowadzeniem do biblioteki PyTest.
Jeśli to Cię interesuje to zapraszam również na swoje social media.

Jak i do ewentualnego postawienia mi kawy :)

Podtypowanie strukturalne

Jeśli określimy typ jawnie, np przez dziedziczenie to mamy doczynienia z podtypowaniem nominalnym. Jeśli jednak typu nie określimy jawnie, to typ może zostać określony na podstawie struktury i wtedy mamy podtypowanie strukturalne. Zatrzymajmy się na chwilę przy podtypowaniu strukturalnym. Zerknij na kod:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from typing import Protocol, runtime_checkable


@runtime_checkable
class OwnObjectProto(Protocol):
  def hello(self, name: str) -> str:
      pass


class MyObject:
  def hello(self, name: str) -> str:
      return f"Hello {name}"


print(isinstance(MyObject(), OwnObjectProto))

Jak myślisz, co wyświetli ten kod? Pewnie się domyślasz, że True. Nastąpiło tutaj dynamiczne określenie typu obiektu klasy MyObject w oparciu o jego strukturę. Zauważ, że klasa nie dziedziczy z żadnej klasy. Python zobaczył, że jego struktura spełnia zdefiniowany wyżej protokół, więc w myśl „duck typing” – skoro robi „hello” jak zdefiniowany protokół, to go spełnia, tym samym, jest to instancja tego typu.

Rozwiązanie jest o tyle fajne, że w porównaniu do udawanych interfejsów, protokoły nie niosą za sobą implementacji a jedynie abstrakcję, to pozwala nam zamknąć sobie drogę do mieszania tych dwóch elementów. Minusem jest oczywiście to, że określanie typu w ten sposób jest tak bardzo niejawne, że aż trochę magiczne.

Mypy

Python, mimo, że udostępnia mechanizm typowania, to nie umie sprawdzać poprawności typów. Najpopularniejszą biblioteką która pozwala nam na to jest mypy. Zerknijmy na taki kod:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from typing import Protocol, runtime_checkable


@runtime_checkable
class OwnObjectProto(Protocol):
  def hello(self, name: str) -> str:
      return 'Hello'


class MyObject:
  def hello(self, name: str) -> str:
      return f"Hello {name}"

  def second_method(self):
      pass


class NotMyObject:
  def second_method(self):
      pass


def func(val: OwnObjectProto) -> None:
  print(val.hello("World!"))


func(MyObject())
func(NotMyObject())

widać tu gołym okiem, że w drugim wywołaniu funkcji func, przekazujemy do niej coś co nie jest przez nią oczekiwane. Jednak nie zawsze jest to tak widoczne. Na pomoc przychodzi wspomniana wcześniej biblioteka mypy, ponieważ uruchamiając ją, otrzymujemy komunikat:

b.py:28: error: Argument 1 to „func” has incompatible type „NotMyObject”; expected „OwnObjectProto”
Found 1 error in 1 file (checked 1 source file)

I drugi przykład kodu:

1
2
3
4
5
6
7
8
9
10
11
12
from abc import ABC, abstractmethod


class AbstractClass(ABC):
  @abstractmethod
  def method1(self, val: int) -> str:
      pass


class ConcreteClass(AbstractClass):
  def method1(self, val: int) -> int:
      return 2

na którym widać, że przeciążona metoda nie jest kompatybilna z metodą z klasy nadrzędnej. Komunikat mypy’a ładnie nas o tym informuje:

b.py:11: error: Return type „int” of „method1” incompatible with return type „str” in supertype „AbstractClass”
Found 1 error in 1 file (checked 1 source file)

Podsumowanie

Mam nadzieję, że wpis przybliżył nieco elementy takie jak klasy abstrakcyjne, interfejsy i to magiczne podtypowanie strukturalne. Jeśli masz jakieś odczucia, przemyślenia i doświadczenie odnośnie tych narzędzi, zostaw komentarz, rozszerzanie zakresu artykułu zawsze są mile widziane.

Dzięki za wizytę,
Mateusz Mazurek
Mateusz M.

Pokaż komentarze

Ostatnie wpisy

Podsumowanie: maj, czerwiec, lipiec i sierpień 2024

Oj daaawnoo mnie tu nie było. Ale wakacje to był czas dużej liczby intensywnych wyjazdów i tak naprawdę, dopiero jakoś… Read More

2 miesiące ago

Podsumowanie: kwiecień 2024

Cześć! Zapraszam na krótkie podsumowanie kwietnia. Wyjazd do Niemiec A dokładniej pod granicę z Francją. Chrześnica miała pierwszą komunię. Po… Read More

6 miesięcy ago

Podsumowanie: luty i marzec 2024

Ostatnio tygodnie były tak bardzo wypełnione, że nie udało mi się napisać nawet krótkiego podsumowanie. Więc dziś zbiorczo podsumuję luty… Read More

7 miesięcy ago

Podsumowanie: styczeń 2024

Zapraszam na krótkie podsumowanie miesiąca. Książki W styczniu przeczytałem "Homo Deus: Historia jutra". Książka łudząco podoba do wcześniejszej książki tego… Read More

9 miesięcy ago

Podsumowanie roku 2023

Cześć! Zapraszam na podsumowanie roku 2023. Książki Zacznijmy od książek. W tym roku cel 35 książek nie został osiągnięty. Niemniej… Read More

10 miesięcy ago

Podsumowanie: grudzień 2023

Zapraszam na krótkie podsumowanie miesiąca. Książki W grudniu skończyłem czytać Mein Kampf. Nudna książka. Ciekawsze fragmenty można by było streścić… Read More

10 miesięcy ago