Vai al contenuto principale

Introduzione all'ereditarietà multipla e super()

Guida introduttiva per Pythonista a ereditarietà multipla, funzione super() e come affrontare il diamond problem.
· 8 min leggi

Scopri con l'IA

Panoramica rapida dell'ereditarietà

Man mano che i tuoi progetti e pacchetti Python crescono, prima o poi vorrai usare le classi e applicare il principio DRY (don't repeat yourself). L'ereditarietà è un ottimo modo per creare una classe a partire da un'altra e restare DRY. In questo post tratteremo concetti più avanzati di ereditarietà; l'ereditarietà di base non verrà approfondita. Faremo una breve introduzione, ma ci sono guide molto migliori e dettagliate là fuori. Ecco alcune risorse per iniziare: corso Object-Oriented Programming in Python e Python Object-Oriented Programming (OOP): Tutorial.

Dunque, cos'è l'ereditarietà di classe? Come in genetica, una classe figlia può "ereditare" attributi e metodi da una classe genitore. Vediamo subito un esempio in codice. Nel blocco seguente mostreremo l'ereditarietà con una classe Child che eredita da una classe Parent.

Input

class Parent:
    def __init__(self):
        self.parent_attribute = 'I am a parent'

    def parent_method(self):
        print('Back in my day...')


# Create a child class that inherits from Parent
class Child(Parent):
    def __init__(self):
        Parent.__init__(self)
        self.child_attribute = 'I am a child'


# Create instance of child
child = Child()

# Show attributes and methods of child class
print(child.child_attribute)
print(child.parent_attribute)
child.parent_method()

Output

I am a child
I am a parent
Back in my day...

Vediamo che la classe Child ha "ereditato" attributi e metodi dalla classe Parent. Senza alcun lavoro da parte nostra, Parent.parent_method fa parte della classe Child. Per ottenere i benefici del metodo Parent.__init__() abbiamo dovuto chiamarlo esplicitamente e passare self. Questo perché, aggiungendo un metodo __init__ a Child, abbiamo sovrascritto l'__init__ ereditato.

Detta questa breve e non esaustiva panoramica, entriamo nel vivo del post.

Introduzione a super

Nel caso più semplice, la funzione super può sostituire la chiamata esplicita a Parent.__init__(self). Il nostro esempio introduttivo della prima sezione può essere riscritto con super come vedi sotto. Nota che il blocco seguente è in Python 3; le versioni precedenti usano una sintassi leggermente diversa. Inoltre, l'output è stato omesso perché identico al primo blocco.

class Parent:
    def __init__(self):
        self.parent_attribute = 'I am a parent'

    def parent_method(self):
        print('Back in my day...')


# Create a child class that inherits from Parent
class Child(Parent):
    def __init__(self):
        super().__init__()
        self.child_attribute = 'I am a parent'


# Create instance of child
child = Child()

# Show attributes and methods of child class
print(child.child_attribute)
print(child.parent_attribute)
child.parent_method()

A dire il vero, in questo caso super offre pochi, se non nulli, vantaggi. A seconda del nome della classe genitore potremmo risparmiare qualche battuta e non dobbiamo passare self alla chiamata di __init__. Qui sotto alcuni pro e contro dell'uso di super in casi di ereditarietà singola.

Contro

Si può sostenere che usare super qui renda il codice meno esplicito. Rendere il codice meno esplicito viola The Zen of Python, che afferma: "Esplicito è meglio che implicito".

Pro

C'è un argomento di manutenibilità a favore di super anche con ereditarietà singola. Se per qualsiasi motivo la tua classe figlia cambia il proprio schema di ereditarietà (ad esempio cambia la classe genitore o si passa a un'ereditarietà multipla), non serve cercare e sostituire tutti i riferimenti sparsi a ParentClass.method_name(); l'uso di super farà propagare le modifiche con il solo cambiamento nella dichiarazione di class.

super ed ereditarietà multipla

Prima di addentrarci in ereditarietà multipla e super... Attenzione, la cosa può diventare piuttosto strana e complicata.

Per cominciare, cos'è l'ereditarietà multipla? Finora gli esempi hanno mostrato una singola classe figlia che eredita da una singola classe genitore. Nell'ereditarietà multipla ci sono più classi genitore. Una classe figlia può ereditare da 2, 3, 10, ecc. classi genitore.

Qui i vantaggi di super diventano più chiari. Oltre a risparmiare battute evitando di scrivere i nomi delle diverse classi genitore, ci sono benefici sottili nell'usare super con schemi di ereditarietà multipla. In breve: se userai l'ereditarietà multipla, usa super.

Ereditarietà multipla senza super

Vediamo un esempio di ereditarietà multipla che evita di modificare i metodi delle classi genitore e quindi evita super.

Input

class B:
    def b(self):
        print('b')


class C:
    def c(self):
        print('c')


class D(B, C):
    def d(self):
        print('d')


d = D()
d.b()
d.c()
d.d()

Output

b
c
d

Ordine di risoluzione multipla

Questo output non sorprende, dato il concetto di ereditarietà multipla. D ha ereditato i metodi x e z dalle sue classi genitore, e tutto fila liscio... per ora.

E se sia B che C avessero un metodo con lo stesso nome? Qui entra in gioco un concetto chiamato "ordine di risoluzione multipla", o MRO. L'MRO di una classe figlia decide dove Python cercherà un certo metodo e quale metodo verrà chiamato in caso di conflitto.

Vediamo un esempio.

Input

class B:
    def x(self):
        print('x: B')


class C:
    def x(self):
        print('x: C')


class D(B, C):
    pass


d = D()
d.x()
print(D.mro())

Output

x: B
[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class 'object'>]

Quando chiamiamo il metodo ereditato x, vediamo solo l'output ereditato da B. Possiamo vedere l'MRO della nostra classe D chiamando il metodo di classe mro. Dall'output di D.mro() apprendiamo quanto segue: il programma cercherà di chiamare i metodi di D per default, poi passerà a B, quindi a C e infine a object. Se non viene trovato in nessuno di questi posti, otterremo l'errore che D non ha il metodo richiesto.

Vale la pena notare che, per impostazione predefinita, ogni classe eredita da object, che si trova in coda a ogni MRO.

Ereditarietà multipla, super e il diamond problem

Qui sotto c'è un esempio di uso di super per gestire l'MRO degli init in modo vantaggioso. Nell'esempio creiamo una serie di classi per l'elaborazione del testo e combiniamo la loro funzionalità in un'altra classe con ereditarietà multipla. Creeremo 4 classi e la struttura di ereditarietà seguirà lo schema nel diagramma qui sotto.

Nota: questa struttura è a scopo illustrativo e, salvo vincoli, ci sarebbero modi migliori per implementare questa logica.

diamond problem

Questo è in realtà un esempio del "diamond problem" dell'ereditarietà multipla. Il nome deriva ovviamente dalla forma del disegno e dal fatto che è un problema piuttosto intricato.

Sotto trovi il design scritto usando super.

Input

class Tokenizer:
    """Tokenize text"""
    def __init__(self, text):
        print('Start Tokenizer.__init__()')
        self.tokens = text.split()
        print('End Tokenizer.__init__()')


class WordCounter(Tokenizer):
    """Count words in text"""
    def __init__(self, text):
        print('Start WordCounter.__init__()')
        super().__init__(text)
        self.word_count = len(self.tokens)
        print('End WordCounter.__init__()')


class Vocabulary(Tokenizer):
    """Find unique words in text"""
    def __init__(self, text):
        print('Start init Vocabulary.__init__()')
        super().__init__(text)
        self.vocab = set(self.tokens)
        print('End init Vocabulary.__init__()')


class TextDescriber(WordCounter, Vocabulary):
    """Describe text with multiple metrics"""
    def __init__(self, text):
        print('Start init TextDescriber.__init__()')
        super().__init__(text)
        print('End init TextDescriber.__init__()')


td = TextDescriber('row row row your boat')
print('--------')
print(td.tokens)
print(td.vocab)
print(td.word_count)

Output

Start init TextDescriber.__init__()
Start WordCounter.__init__()
Start init Vocabulary.__init__()
Start Tokenizer.__init__()
End Tokenizer.__init__()
End init Vocabulary.__init__()
End WordCounter.__init__()
End init TextDescriber.__init__()
--------
['row', 'row', 'row', 'your', 'boat']
{'boat', 'your', 'row'}
5

Per prima cosa, vediamo che la classe TextDescriber ha ereditato tutti gli attributi dell'albero genealogico di classi. Grazie all'ereditarietà multipla possiamo "combinare" la funzionalità di più classi.

Parliamo ora delle stampe generate dai metodi init delle classi:

Ogni metodo __init__ è stato chiamato una e una sola volta.

La classe TextDescriber eredita da 2 classi che ereditano da Tokenizer. Perché Tokenizer.__init__ non è stato chiamato due volte?

Se sostituissimo tutte le chiamate a super con il vecchio metodo, finiremmo con 2 chiamate a Tokenizer.__init__. Le chiamate a super "ragionano" un po' di più sul nostro schema e saltano il viaggio extra verso A.

Ogni metodo __init__ è iniziato prima che qualcun altro fosse terminato.

L'ordine di avvio e termine di ciascun __init__ è da notare se stai cercando di impostare un attributo che entra in conflitto di nome con un'altra classe genitore. L'attributo verrà sovrascritto e la cosa può diventare molto confusa.

Nel nostro caso, abbiamo evitato conflitti di nomi tra attributi ereditati, quindi tutto funziona come previsto.

Per ribadirlo, il diamond problem può complicarsi rapidamente e portare a risultati inattesi. Come spesso in programmazione, è meglio evitare design complicati.

Cosa abbiamo imparato

  • Abbiamo visto la funzione super e come possa sostituire ParentName.method nell'ereditarietà singola. Può essere una pratica più manutenibile.
  • Abbiamo visto l'ereditarietà multipla e come si possa trasferire la funzionalità di più classi genitore a una singola classe figlia.
  • Abbiamo visto l'ordine di risoluzione multipla e come decida cosa succede nell'ereditarietà multipla quando c'è un conflitto di nomi tra metodi genitori.
  • Abbiamo visto il diamond problem e un esempio di come l'uso di super lo attraversi correttamente.

Argomenti

Correlato