Klasa zbudowana na innej klasie
Dziedziczenie pozwala zdefiniować nową klasę jako rozszerzenie istniejącej. Istniejąca klasa to rodzic (nazywany też klasą bazową albo nadklasą), a nowa to dziecko (podklasa). Dziecko dostaje wszystkie metody i atrybuty klasy rodzica bez ich powtarzania, a może dodać własne albo podmienić te, które mają działać inaczej.
Rodzica deklaruje się w nawiasach po nazwie klasy:
Wynik:
Rex is an animal
Woof
Dog nie ma własnego __init__ ani describe. Gdy wywołujesz Dog("Rex"), Python szuka __init__ w Dog, nie znajduje go i używa tego z Animal. Tak samo wygląda wyszukiwanie przy rex.describe(). Przy rex.speak() znajduje speak najpierw w Dog i na tym kończy.
Ta kolejność wyszukiwania to cały mechanizm: Python szuka w klasie samego obiektu, potem w rodzicu, potem w rodzicu rodzica i używa pierwszego trafienia. Jeśli klasy są dla ciebie nowe, strona o klasach najpierw omawia __init__, self i metody.
Nadpisywanie metody
Metoda w dziecku o tej samej nazwie co metoda w rodzicu zastępuje ją dla instancji dziecka. Nazywa się to nadpisywaniem (overriding). Każda podklasa może odpowiedzieć na to samo wywołanie po swojemu:
Wynik:
Rectangle with area 12
Square with area 25
Shape with area 0
summary jest zdefiniowana raz, w Shape, ale wywołuje self.area(). Ponieważ self to faktyczny obiekt, Rectangle wykonuje Rectangle.area, a Square znajduje area u swojego rodzica Rectangle. Kod rodzica wywołuje metodę dziecka, nie wiedząc nawet, że dziecko istnieje. Właśnie dlatego dziedziczenie przydaje się do współdzielenia wspólnego schematu działania, w którym każda podklasa uzupełnia szczegóły.
Wywołanie rodzica przez super()
Gdy dziecko definiuje własne __init__, __init__ rodzica przestaje uruchamiać się samo. Jeśli dziecko nadal potrzebuje przygotowania z rodzica, wywołaj je przez super().__init__():
Wynik:
Ada 1050.0
super() zwraca obiekt, który szuka metod, zaczynając od rodzica bieżącej klasy. Dlatego super().__init__(owner, balance) wykonuje Account.__init__ na tym samym obiekcie, a potem istnieją self.owner i self.balance.
Pomiń tę linię, a obiekt będzie zbudowany tylko w połowie:
Wynik:
0.05
AttributeError: 'SavingsAccount' object has no attribute 'owner'
Zamiast tego możesz napisać Account.__init__(self, owner) i starszy kod często tak robi. Lepsze jest super(), bo nie powtarza nazwy rodzica i poprawnie współpracuje z wielodziedziczeniem, opisanym niżej.
Rozszerzanie metody zamiast jej zastępowania
super() działa w każdej metodzie, nie tylko w __init__. Wywołanie wersji rodzica wewnątrz nadpisanej metody pozwala dodać zachowanie wokół niej zamiast pisać ją od nowa:
Wynik:
started
[12:00] started
INFO [12:00] started
Każdy poziom dodaje jeden element, a resztę przekazuje w górę. Jeśli format rodzica kiedyś się zmieni, obie klasy potomne przejmą tę zmianę.
isinstance i issubclass
Instancja podklasy jest też instancją każdej klasy powyżej niej. isinstance(obj, cls) sprawdza tę relację, a issubclass(child, parent) sprawdza ją między dwiema klasami:
Wybieraj isinstance zamiast bezpośredniego porównywania type(obj). Sprawdzenie takie jak type(obj) is Animal odrzuca każdą podklasę, więc psuje się, gdy tylko ktoś doda Dog.
Każda klasa, którą piszesz, dziedziczy po object, nawet jeśli tego nie napiszesz. class Animal: to to samo co class Animal(object):. Stąd biorą się domyślne metody, takie jak __str__, __repr__ i __eq__, i dlatego nadpisanie __str__ zmienia to, co pokazuje print():
Wynik:
(2, 3)
True
Wymuszenie implementacji metody w podklasach
Czasem rodzica nie należy nigdy używać bezpośrednio, tylko jako szablonu. Moduł abc oznacza taką klasę jako abstrakcyjną. Każdą metodę udekorowaną @abstractmethod trzeba nadpisać, zanim da się utworzyć instancję klasy:
Pierwsze trzy linie wyniku to eksport:
exporting 2 rows
1,2
3,4
Ostatnia linia to TypeError, który wymienia Exporter i brakującą metodę export (dokładne sformułowanie różni się między wersjami Pythona). Błąd wychodzi na jaw w chwili tworzenia obiektu, a nie później, gdy w końcu wywołuje się export.
Wielodziedziczenie i MRO
Klasa może mieć więcej niż jednego rodzica. Dziedziczy wtedy po nich wszystkich:
Wynik:
paddling flapping
swims
['Duck', 'Swimmer', 'Flyer', 'object']
Obaj rodzice definiują move, a Duck dostaje wersję ze Swimmer. Python rozstrzyga to według kolejności rozwiązywania metod (MRO): jednej listy klas zapisanej w Duck.__mro__, którą każde wyszukiwanie atrybutu przechodzi od początku do końca. Zaczyna się od samej klasy, zachowuje rodziców w kolejności, w jakiej ich zapisano, stawia każdą klasę przed jej własnymi rodzicami i kończy się na object. Zamień kolejność na class Duck(Flyer, Swimmer), a move zwróci "flies".
super() korzysta z tej samej listy. W metodzie Swimmer wywołanie super() oznacza "następną klasę po Swimmer w MRO klasy obiektu", czyli dla Duck jest to Flyer, a nie object. Dzięki temu współpracujące klasy mogą każda wywołać super(), a każda klasa w łańcuchu wykona się dokładnie raz, nawet w układzie rombu, gdzie dwóch rodziców ma wspólnego dziadka.
W praktyce wielodziedziczenia używa się głównie do mixinów: małych klas, które dodają jedną możliwość, jak JsonMixin dodający metodę to_json, łączonych z jednym głównym rodzicem. Głębokie hierarchie z kilkoma pełnymi rodzicami trudno śledzić i rzadko są potrzebne.
Dziedziczenie czy kompozycja?
Dziedziczenie modeluje relację "jest". SavingsAccount jest Account: każdy kod, który działa z Account, powinien bez zmian działać z nim. Gdy to zdanie brzmi źle, prawdopodobnie potrzebujesz kompozycji, która modeluje relację "ma": obiekt przechowuje inny obiekt jako atrybut i go wywołuje.
Wynik:
Roadster: 300 hp engine running
class Car(Engine) też by działało, ale twierdziłoby, że samochód jest rodzajem silnika, dałoby Car wszystkie metody silnika i uniemożliwiłoby podmianę silnika bez tworzenia innej klasy. Przy kompozycji części pozostają osobne, a wymiana silnika sprowadza się do przekazania innego obiektu.
Kilka sygnałów, że dziedziczenie to złe narzędzie:
- Dziecko nadpisuje większość metod rodzica albo niektóre z nich wyłącza.
- Dziedziczysz tylko po to, żeby użyć jednej czy dwóch metod pomocniczych.
- Hierarchia rozrasta się ponad dwa czy trzy poziomy i trzeba czytać kilka plików, żeby zobaczyć, co robi metoda.
Częste błędy
- Zapominanie o
super().__init__().__init__dziecka zastępuje ten z rodzica, więc atrybuty rodzica nigdy nie zostają ustawione, a później pojawia sięAttributeError. - Przekazywanie
selfw wywołaniachsuper(). Piszsuper().__init__(name), a niesuper().__init__(self, name).super()już wiąże metodę z bieżącym obiektem. - Niezgodne sygnatury przy nadpisywaniu. Jeśli wspólny kod wywołuje
speak(self)rodzica, tospeak(self, volume)w dziecku z dodatkowym wymaganym argumentem psuje ten kod. Utrzymuj zgodność nadpisanych metod albo nadawaj nowym parametrom wartości domyślne. - Sprawdzanie
type(obj) == SomeClass. Ignoruje podklasy. Używajisinstance. - Używanie dziedziczenia do współdzielenia kodu między niepowiązanymi klasami. Przenieś wspólny kod do funkcji albo obiektu pomocniczego.
Najczęściej zadawane pytania
Jak dziedziczyć po klasie w Pythonie?
Umieść klasę nadrzędną w nawiasach po nazwie nowej klasy: class Dog(Animal):. Dog dostaje wszystkie metody i atrybuty klasy Animal i może dodawać nowe albo nadpisywać istniejące.
Co robi super().__init__()?
Wywołuje __init__ klasy nadrzędnej, żeby rodzic mógł ustawić własne atrybuty. Gdy podklasa definiuje własne __init__, Python nie wywołuje automatycznie tego z rodzica, więc wywołujesz super().__init__(...) z argumentami, których potrzebuje rodzic, a potem ustawiasz dodatkowe atrybuty podklasy.
Czym różni się isinstance od type w Pythonie?
isinstance(obj, Animal) daje True dla Animal i dla instancji każdej podklasy Animal. type(obj) is Animal daje True tylko wtedy, gdy obiekt utworzono z samego Animal. Wybieraj isinstance, bo działa dalej, gdy ktoś doda podklasę.
Czy Python obsługuje wielodziedziczenie?
Tak: class C(A, B): dziedziczy po obu klasach. Gdy obaj rodzice definiują tę samą metodę, Python wybiera jedną zgodnie z kolejnością rozwiązywania metod (MRO), którą możesz podejrzeć przez C.__mro__. Na liście jest najpierw C, potem rodzice w kolejności zapisu, a na końcu object.
Kiedy użyć kompozycji zamiast dziedziczenia?
Dziedziczenia używaj dla relacji "jest", gdy podklasy można użyć wszędzie tam, gdzie oczekiwany jest rodzic (SavingsAccount jest Account). Kompozycji, czyli przechowywania obiektu jako atrybutu, używaj dla relacji "ma" (Car ma Engine). W razie wątpliwości kompozycję łatwiej później zmienić.