2.22. Ereditarietà

L’ereditarietà permette a una classe di estendere un’altra classe – partire con tutti i suoi metodi e attributi, poi aggiungere o sovrascrivere ciò che è diverso. L’originale è chiamato la base (o genitore); l’estensione è chiamata sottoclasse.

Una classe base Shape che punta in basso verso una sottoclasse Square; Square sovrascrive un metodo (area) ed eredita il resto.

Square riutilizza ogni metodo di Shape e sovrascrive quelli che deve specializzare.

2.22.1. Definire una sottoclasse

La classe base va nelle parentesi sulla riga 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())

Output:

square with area 16

Square eredita describe da Shape invariato e sovrascrive area per restituire un valore reale. Il describe della base funziona ancora perché chiama self.area() – che a runtime si risolve nella versione sovrascritta.

2.22.2. super()

super() restituisce un proxy che permette a un metodo di chiamare la versione della classe base di un metodo. L’uso più comune è chiamare l”__init__ della base da un __init__ della sottoclasse, così che la base possa inizializzare i propri attributi:

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

Dimenticare super().__init__ è una comune fonte di bug: gli attributi che la base avrebbe impostato non vengono mai impostati, e i metodi ereditati che dipendono da essi falliscono in seguito.

2.22.2.1. La classe base implicita

Ogni classe eredita implicitamente da object, la radice della gerarchia dei tipi di Python. Metodi come __repr__, __eq__ e il meccanismo dietro l’accesso agli attributi provengono tutti da lì anche quando una classe non dichiara alcuna base. Scrivere class Foo(object): esplicitamente e scrivere class Foo: sono equivalenti nel Python moderno; quest’ultima è la forma convenzionale.

2.22.3. Quando l’ereditarietà aiuta – e quando no

Usa l’ereditarietà quando una classe è genuinamente un tipo più specifico di un’altra, condividendone la maggior parte del comportamento. Il test classico è la verifica «is-a»: un Square è uno Shape.

Quando due classi semplicemente condividono qualche helper, l’ereditarietà è eccessiva. Ricorri invece alla composizione: fai in modo che una classe contenga un’istanza dell’altra come attributo e la usi attraverso quell’attributo. La forma è «has-a», non «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")

Entrambe funzionano. La versione che compone è di solito migliore perché:

  • Mantiene piccole le interfacce. WorkerComposes espone solo run e logger. WorkerInherits espone anche log – i chiamanti possono scrivere worker.log(...) direttamente, che ciò fosse voluto o meno.

  • Disaccoppia i cicli di vita. Il logger può essere sostituito, condiviso tra worker o costruito in modo diverso per ogni worker, tutto senza toccare la classe Worker. Una sottoclasse è imbullonata alla sua base.

  • Evita le collisioni di nomi di metodo. Due basi che definiscono entrambe run collidono quando una classe cerca di ereditare da entrambe; due attributi non collidono mai.

Una regola pratica:

  • Usa l’ereditarietà quando la sottoclasse è davvero un tipo della base e ogni metodo della base ha senso anche su di essa – Square è uno Shape; MemoryError è una Exception.

  • Usa la composizione per tutto il resto – quando vuoi semplicemente il comportamento di un’altra classe, non la sua identità. La maggior parte del codice reale usa la composizione molto più dell’ereditarietà.