2.22. Öröklődés

Az öröklődés lehetővé teszi, hogy egy osztály kiterjesszen egy másik osztályt – annak összes metódusából és attribútumából kiindulva, majd hozzáadva vagy felülírva azt, ami eltér. Az eredetit bázisnak (vagy szülőnek) nevezik; a kiterjesztést pedig alosztálynak.

Egy Shape bázisosztály lefelé mutat egy Square alosztályra; a Square felülír egy metódust (area), a többit pedig örökli.

A Square a Shape minden metódusát újrahasználja, és csak azokat írja felül, amelyeket specializálnia kell.

2.22.1. Alosztály definiálása

A bázisosztály a class sor zárójeleibe kerül:

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())

Kimenet:

square with area 16

A Square változatlanul örökli a describe metódust a Shape osztálytól, és felülírja az area metódust, hogy valódi értéket adjon vissza. A bázis describe metódusa továbbra is működik, mert meghívja a self.area() metódust – ami futási időben a felülírásra oldódik fel.

2.22.2. super()

A super() egy olyan proxyt ad vissza, amely lehetővé teszi, hogy egy metódus meghívja egy metódus bázisosztálybeli változatát. A leggyakoribb felhasználás a bázis __init__ meghívása egy alosztály __init__ metódusából, hogy a bázis inicializálhassa a saját attribútumait:

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

A super().__init__ elfelejtése a hibák gyakori forrása: a bázis által beállított attribútumok soha nem állítódnak be, az ezekre támaszkodó örökölt metódusok pedig később összeomlanak.

2.22.2.1. Az implicit bázisosztály

Minden osztály implicit módon az object osztálytól örököl, amely a Python típushierarchiájának gyökere. Az olyan metódusok, mint a __repr__, a __eq__, és az attribútumhozzáférés mögötti gépezet mind innen származnak, még akkor is, ha egy osztály nem deklarál bázist. A class Foo(object): explicit kiírása és a class Foo: kiírása egyenértékű a modern Pythonban; az utóbbi a szokásos forma.

2.22.3. Mikor segít az öröklődés – és mikor nem

Akkor használj öröklődést, amikor az egyik osztály valóban egy másiknak egy specifikusabb fajtája, és a viselkedés nagy részén osztozik. A klasszikus teszt az „is-a” (egy-fajtája) ellenőrzés: egy Square az egy Shape.

Amikor két osztály csak véletlenül osztozik néhány segédfüggvényen, az öröklődés túlzás. Inkább nyúlj kompozícióhoz: az egyik osztály attribútumként tartalmazza a másik egy példányát, és azon az attribútumon keresztül használja. Az alakja „has-a” (van-neki), nem „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")

Mindkettő működik. A kompozíciós változat általában jobb, mert:

  • Kicsiben tartja a felületeket. A WorkerComposes csak a run és a logger elemet teszi elérhetővé. A WorkerInherits a log metódust is elérhetővé teszi – a hívók közvetlenül írhatják, hogy worker.log(...), akár szándékos volt ez, akár nem.

  • Szétválasztja az élettartamokat. A logger lecserélhető, megosztható a workerek között, vagy workerenként eltérően felépíthető, mindezt a Worker osztály érintése nélkül. Egy alosztály a bázisához van rögzítve.

  • Elkerüli a metódusnév-ütközéseket. Két olyan bázis, amelyek mindegyike definiál egy run metódust, ütközik, amikor egy osztály mindkettőtől próbál örökölni; két attribútum soha nem ütközik.

Egy gyakorlati ökölszabály:

  • Akkor használj öröklődést, amikor az alosztály valóban egy fajtája a bázisnak, és a bázis minden metódusa rajta is értelmes – a Square az egy Shape; a MemoryError az egy Exception.

  • Minden másra használj kompozíciót – amikor csak egy másik osztály viselkedésére van szükséged, nem az identitására. A legtöbb valós kód sokkal többször használ kompozíciót, mint öröklődést.