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ą.

Klasa bazowa Shape wskazująca w dół na podklasę Square; Square nadpisuje jedną metodę (area) i dziedziczy resztę.

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. WorkerComposes udostępnia tylko run i logger. WorkerInherits udostę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 – Square jest typu Shape; MemoryError jest typu Exception.

  • 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.