2.22. Nasljeđivanje

Nasljeđivanje omogućuje klasi da proširi drugu klasu – da počne sa svim njezinim metodama i atributima, a zatim doda ili nadjača ono što je drukčije. Original se naziva baza (ili roditelj); proširenje se naziva podklasa.

Bazna klasa Shape pokazuje prema dolje na podklasu Square; Square nadjačava jednu metodu (area) i nasljeđuje ostalo.

Square ponovno koristi svaku metodu na Shape i nadjačava one koje treba specijalizirati.

2.22.1. Definiranje podklase

Bazna klasa ide u zagrade na retku 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())

Izlaz:

square with area 16

Square nepromijenjeno nasljeđuje describe od Shape i nadjačava area da vrati stvarnu vrijednost. Bazna metoda describe i dalje radi jer poziva self.area() – što se u vrijeme izvođenja razrješava na nadjačanu metodu.

2.22.2. super()

super() vraća proxy koji omogućuje metodi da pozove baznu verziju metode. Najčešća upotreba je pozivanje baznog __init__ iz __init__ podklase kako bi baza inicijalizirala vlastite atribute:

class Square(Shape):
    def __init__(self, side):
        super().__init__("square")    # run Shape.__init__
        self.side = side

Zaboravljanje super().__init__ čest je izvor grešaka: atributi koje bi baza postavila nikad se ne postave, a naslijeđene metode koje se na njih oslanjaju kasnije se sruše.

2.22.2.1. Implicitna bazna klasa

Svaka klasa implicitno nasljeđuje od object, korijena Pythonove hijerarhije tipova. Metode poput __repr__, __eq__ i mehanizam iza pristupa atributima sve potječu odande čak i kad klasa ne deklarira nijednu bazu. Eksplicitno pisanje class Foo(object): i pisanje class Foo: ekvivalentni su u modernom Pythonu; potonji je konvencionalni oblik.

2.22.3. Kada nasljeđivanje pomaže – a kada ne

Koristi nasljeđivanje kad je jedna klasa stvarno specifičnija vrsta druge, dijeleći većinu ponašanja. Klasični test je provjera „jest-a”: Square jest Shape.

Kad se dvije klase tek slučajno dijele nekoliko pomoćnika, nasljeđivanje je pretjerano. Posegni umjesto toga za kompozicijom: neka jedna klasa drži instancu druge kao atribut i koristi je kroz taj atribut. Oblik je „ima-a”, ne „jest-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")

Oba rade. Verzija s kompozicijom obično je bolja jer:

  • Drži sučelja malenima. WorkerComposes izlaže samo run i logger. WorkerInherits izlaže i log – pozivatelji mogu izravno pisati worker.log(...), bilo da je to bilo namjeravano ili ne.

  • Razdvaja životne vijekove. Logger se može zamijeniti, dijeliti među radnicima ili konstruirati drukčije po radniku, sve to bez diranja klase Worker. Podklasa je pričvršćena za svoju bazu.

  • Izbjegava sudare imena metoda. Dvije baze koje obje definiraju run sudaraju se kad klasa pokuša naslijediti od obje; dva atributa nikad se ne sudaraju.

Praktično pravilo:

  • Koristi nasljeđivanje kad podklasa stvarno jest vrsta baze i kad svaka metoda na bazi ima smisla i na njoj – Square jest Shape; MemoryError jest Exception.

  • Koristi kompoziciju za sve ostalo – kad želiš samo ponašanje druge klase, a ne njezin identitet. Većina stvarnog koda koristi kompoziciju daleko više nego nasljeđivanje.