Ga naar hoofdinhoud

Introductie tot meervoudige overerving & super()

Een intro voor Pythonista’s tot meervoudige overerving, de functie super() en hoe je het diamond-probleem navigeert.
· 8 min lezen

Verken met AI

Korte introductie tot overerving

Naarmate je Python-projecten en -packages groeien, wil je onvermijdelijk klassen gebruiken en daarbij het DRY-principe (don’t repeat yourself) toepassen. Klassenovererving is een geweldige manier om een klasse te maken op basis van een andere klasse om DRY te blijven. Deze post behandelt meer geavanceerde concepten van overerving; basisovererving komt niet uitgebreid aan bod. We doen een korte intro, maar er zijn veel betere, gedetailleerde introducties te vinden. Hier zijn wat bronnen om te beginnen: de cursus Object-Oriented Programming in Python & Python Object-Oriented Programming (OOP): Tutorial.

Wat is klassenovererving precies? Net als bij genetica kan een child-klasse attributen en methoden ‘erven’ van een parent. Laten we meteen in wat code duiken voor een voorbeeld. In het onderstaande codeblok laten we overerving zien met een Child-klasse die erft van een Parent-klasse.

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...

We zien dat de Child-klasse attributen en methoden ‘erft’ van de Parent-klasse. Zonder extra werk aan onze kant maakt Parent.parent_method deel uit van de Child-klasse. Om de voordelen van de Parent.__init__()-methode te krijgen, moesten we de methode expliciet aanroepen en self doorgeven. Dat komt doordat we bij het toevoegen van een __init__-methode aan Child de geërfde __init__ overschreven.

Met die korte, allerminst volledige intro uit de weg, duiken we nu in de kern van de post.

Intro tot super

In het simpelste geval kan de functie super worden gebruikt om de expliciete aanroep Parent.__init__(self) te vervangen. Ons introvoorbeeld uit de eerste sectie kan met super worden herschreven zoals hieronder. Let op: het onderstaande codeblok is geschreven in Python 3; eerdere versies gebruiken een iets andere syntax. Daarnaast is de output weggelaten omdat die identiek is aan het eerste codeblok.

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

Eerlijk gezegd levert super in dit geval weinig tot geen voordeel op. Afhankelijk van de naam van onze parent-klasse besparen we misschien wat toetsaanslagen, en we hoeven self niet door te geven aan de aanroep van __init__. Hieronder staan enkele voor- en nadelen van het gebruik van super bij enkelvoudige overerving.

Nadelen

Je zou kunnen stellen dat het gebruik van super de code hier minder expliciet maakt. Code minder expliciet maken druist in tegen The Zen of Python, waarin staat: “Explicit is better than implicit.”

Voordelen

Er is een onderhoudbaarheidsargument te maken voor super, zelfs bij enkelvoudige overerving. Als om wat voor reden dan ook je child-klasse haar overervingspatroon wijzigt (bijv. de parent-klasse verandert of er wordt overgestapt op meervoudige overerving), dan hoef je niet overal de verwijzingen naar ParentClass.method_name() te zoeken en te vervangen; met super vloeien de wijzigingen automatisch door dankzij de verandering in de class-verklaring.

super en meervoudige overerving

Voordat we in meervoudige overerving en super duiken… Waarschuwing: dit kan best vreemd en ingewikkeld worden.

Om te beginnen: wat is meervoudige overerving? Tot nu toe ging de voorbeeldcode over één child-klasse die erft van één parent-klasse. Bij meervoudige overerving zijn er meerdere parent-klassen. Een child-klasse kan erven van 2, 3, 10, enz. parent-klassen.

Hier worden de voordelen van super duidelijker. Naast het besparen van toetsaanslagen door niet telkens verschillende parent-klassenamen te hoeven refereren, zijn er subtiele voordelen aan het gebruik van super bij patronen van meervoudige overerving. Kort gezegd: als je meervoudige overerving gaat gebruiken, gebruik dan super.

Meervoudige overerving zonder super

Laten we kijken naar een voorbeeld van meervoudige overerving dat het wijzigen van parent-methoden vermijdt en daarmee ook super vermijdt.

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

Multiple-resolution order

Deze output is niet heel verrassend gezien het concept van meervoudige overerving. D erfde de methoden x en z van zijn parent-klassen, en alles is prima… voorlopig.

Maar wat als zowel B als C een methode met dezelfde naam hadden? Dan komt het concept ‘multiple-resolution order’ om de hoek kijken, of kortweg MRO. De MRO van een child-klasse bepaalt waar Python naar een bepaalde methode zoekt en welke methode wordt aangeroepen wanneer er een conflict is.

Laten we een voorbeeld bekijken.

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'>]

Wanneer we de geërfde x-methode aanroepen, zien we alleen de output die is geërfd van B. We kunnen de MRO van onze D-klasse zien door de klassemethode mro aan te roepen. Uit de output van D.mro() leren we het volgende: ons programma probeert standaard D-methoden aan te roepen, valt dan terug op B, daarna op C, en tenslotte op object. Als het daar nergens wordt gevonden, krijgen we de fout dat D de gevraagde methode niet heeft.

Het is vermeldenswaard dat standaard elke klasse erft van object, en dat staat altijd achteraan in elke MRO.

Meervoudige overerving, super en het diamond-probleem

Hieronder staat een voorbeeld van het gebruik van super om de MRO van init op een nuttige manier af te handelen. In het voorbeeld maken we een reeks tekstverwerkingsklassen en combineren we hun functionaliteit in een andere klasse met meervoudige overerving. We maken 4 klassen en de overervingsstructuur volgt het diagram hieronder.

Opmerking: deze structuur is ter illustratie; afgezien van beperkingen zijn er betere manieren om deze logica te implementeren.

diamond problem

Dit is eigenlijk een voorbeeld van het ‘diamond-probleem’ bij meervoudige overerving. De naam is uiteraard gebaseerd op de vorm van het ontwerp en het feit dat het een behoorlijk verwarrend probleem is.

Hieronder is het ontwerp uitgeschreven met gebruik van 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

Allereerst zien we dat de TextDescriber-klasse alle attributen van de klassenstamboom heeft geërfd. Dankzij meervoudige overerving kunnen we de functionaliteit van meer dan één klasse ‘combineren’.

Laten we nu de printregels bespreken die uit de init-methoden van de klassen kwamen:

Elke __init__-methode is precies één keer aangeroepen.

De TextDescriber-klasse erft van 2 klassen die van Tokenizer erven. Waarom is Tokenizer.__init__ niet twee keer aangeroepen?

Als we alle aanroepen van super zouden vervangen door de ouderwetse manier, zouden we 2 aanroepen naar Tokenizer.__init__ krijgen. De aanroepen via super ‘denken’ iets beter mee met ons patroon en slaan het extra rondje langs A over.

Elke __init__-methode begon voordat een andere was afgerond.

De volgorde van de starts en finishes van elke __init__ is het vermelden waard als je een attribuut probeert te zetten dat een naamconflict heeft met een andere parent-klasse. Het attribuut wordt dan overschreven, en dat kan erg verwarrend worden.

In ons geval hebben we naamconflicten met geërfde attributen vermeden, dus alles werkt zoals verwacht.

Nogmaals: het diamond-probleem kan snel ingewikkeld worden en tot onverwachte resultaten leiden. Zoals bij de meeste programmeergevallen is het het beste om complexe ontwerpen te vermijden.

Wat we hebben geleerd

  • We leerden over de functie super en hoe die ParentName.method kan vervangen bij enkelvoudige overerving. Dit kan onderhoudsvriendelijker zijn.
  • We leerden over meervoudige overerving en hoe we de functionaliteit van meerdere parent-klassen kunnen doorgeven aan één child-klasse.
  • We leerden over multiple-resolution order en hoe dat bepaalt wat er gebeurt bij meervoudige overerving wanneer er een naamconflict is tussen parent-methoden.
  • We leerden over het diamond-probleem en zagen een voorbeeld van hoe het gebruik van super het diamond navigeert.

Onderwerpen

Gerelateerd