2.22. Dziedziczenie¶
Dziedziczenie pozwala klasie rozszerzyć inną klasę – zacząć od wszystkich jej metod i atrybutów, a następnie dodać lub nadpisać to, co się różni. Oryginał nazywany jest bazą (lub rodzicem); rozszerzenie nazywane jest podklasą.
Square ponownie wykorzystuje każdą metodę klasy Shape i nadpisuje te, które musi wyspecjalizować.¶
2.22.1. Definiowanie podklasy¶
Klasa bazowa trafia do nawiasów w wierszu class:
class Shape:
def __init__(self, name):
self.name = name
def describe(self):
return self.name + " with area " + str(self.area())
def area(self):
return 0
class Square(Shape):
def __init__(self, side):
super().__init__("square")
self.side = side
def area(self):
return self.side * self.side
s = Square(4)
print(s.describe())
Wynik:
square with area 16
Square dziedziczy describe z Shape bez zmian i nadpisuje area, aby zwracało prawdziwą wartość. Bazowe describe nadal działa, ponieważ wywołuje self.area() – co w czasie wykonania rozwiązuje się do nadpisanej wersji.
2.22.2. super()¶
super() zwraca obiekt pośredniczący, który pozwala metodzie wywołać wersję metody z klasy bazowej. Najczęstszym zastosowaniem jest wywołanie bazowego __init__ z __init__ podklasy, aby baza mogła zainicjalizować własne atrybuty:
class Square(Shape):
def __init__(self, side):
super().__init__("square") # run Shape.__init__
self.side = side
Zapomnienie o super().__init__ to częste źródło błędów: atrybuty, które ustawiłaby baza, nigdy nie zostają ustawione, a dziedziczone metody, które na nich polegają, później się wykładają.
2.22.2.1. Niejawna klasa bazowa¶
Każda klasa niejawnie dziedziczy po object, korzeniu hierarchii typów Pythona. Metody takie jak __repr__, __eq__ oraz mechanizmy stojące za dostępem do atrybutów pochodzą stamtąd nawet wtedy, gdy klasa nie deklaruje żadnej bazy. Napisanie class Foo(object): jawnie oraz napisanie class Foo: są w nowoczesnym Pythonie równoważne; ta druga forma jest formą zwyczajową.
2.22.3. Kiedy dziedziczenie pomaga – a kiedy nie¶
Używaj dziedziczenia, gdy jedna klasa jest naprawdę bardziej konkretnym rodzajem innej, współdzieląc większość zachowania. Klasycznym testem jest sprawdzenie „jest” (is-a): Square jest typu Shape.
Gdy dwie klasy po prostu przypadkiem współdzielą kilka pomocników, dziedziczenie jest przesadą. Sięgnij zamiast tego po kompozycję: niech jedna klasa przechowuje instancję drugiej jako atrybut i używa jej poprzez ten atrybut. Postać brzmi „ma” (has-a), a nie „jest” (is-a):
class Logger:
def log(self, msg):
print("[log]", msg)
# inheritance: Worker IS-A Logger
class WorkerInherits(Logger):
def run(self):
self.log("starting")
# composition: Worker HAS-A Logger
class WorkerComposes:
def __init__(self):
self.logger = Logger()
def run(self):
self.logger.log("starting")
Obie działają. Wersja oparta na kompozycji jest zazwyczaj lepsza, ponieważ:
Utrzymuje małe interfejsy.
WorkerComposesudostępnia tylkorunilogger.WorkerInheritsudostępnia równieżlog– wywołujący mogą pisaćworker.log(...)bezpośrednio, niezależnie od tego, czy było to zamierzone.Rozdziela czasy życia. Logger można podmienić, współdzielić między pracownikami lub konstruować inaczej dla każdego pracownika, a wszystko to bez dotykania klasy
Worker. Podklasa jest przyśrubowana do swojej bazy.Unika kolizji nazw metod. Dwie bazy, które obie definiują
run, kolidują, gdy klasa próbuje dziedziczyć po obu; dwa atrybuty nigdy nie kolidują.
Praktyczna zasada:
Używaj dziedziczenia, gdy podklasa naprawdę jest rodzajem bazy i każda metoda bazy ma na niej również sens –
Squarejest typuShape;MemoryErrorjest typuException.Używaj kompozycji do wszystkiego innego – gdy chcesz tylko zachowania innej klasy, a nie jej tożsamości. Większość rzeczywistego kodu używa kompozycji znacznie częściej niż dziedziczenia.