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.
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.
WorkerComposesespone solorunelogger.WorkerInheritsespone anchelog– i chiamanti possono scrivereworker.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
runcollidono 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è unoShape;MemoryErrorè unaException.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à.