Erweiterbare Programme durch Vererbung

PythonPythonBeginner
Jetzt üben

This tutorial is from open-source community. Access the source code

💡 Dieser Artikel wurde von AI-Assistenten übersetzt. Um die englische Version anzuzeigen, können Sie hier klicken

Einführung

Vererbung ist ein häufig verwendetes Werkzeug zum Schreiben von erweiterbaren Programmen. In diesem Abschnitt wird diese Idee untersucht.

Vererbung

Vererbung wird verwendet, um vorhandene Objekte zu spezialisieren:

class Parent:
  ...

class Child(Parent):
  ...

Die neue Klasse Child wird als abgeleitete Klasse oder Unterklasse bezeichnet. Die Parent-Klasse ist als Basisklasse oder Superklasse bekannt. Parent wird in () nach dem Klassennamen angegeben, class Child(Parent):.

Erweitern

Mit Vererbung übernimmst du eine vorhandene Klasse und:

  • Fügst neue Methoden hinzu
  • Redefinierst einige der vorhandenen Methoden
  • Fügst neuen Attributen zu Instanzen hinzu

Am Ende erweitern Sie vorhandenen Code.

Beispiel

Angenommen, das ist deine Ausgangsklasse:

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    def cost(self):
        return self.shares * self.price

    def sell(self, nshares):
        self.shares -= nshares

Du kannst jeder Teil davon über Vererbung ändern.

Füge eine neue Methode hinzu

class MyStock(Stock):
    def panic(self):
        self.sell(self.shares)

Verwendungsbeispiel.

>>> s = MyStock('GOOG', 100, 490.1)
>>> s.sell(25)
>>> s.shares
75
>>> s.panic()
>>> s.shares
0
>>>

Neudefinieren einer vorhandenen Methode

class MyStock(Stock):
    def cost(self):
        return 1.25 * self.shares * self.price

Verwendungsbeispiel.

>>> s = MyStock('GOOG', 100, 490.1)
>>> s.cost()
61262.5
>>>

Die neue Methode ersetzt die alte. Die anderen Methoden bleiben unberührt. Es ist großartig.

Überschreiben

Manchmal erweitert eine Klasse eine vorhandene Methode, aber möchte die ursprüngliche Implementierung innerhalb der Neudefinition verwenden. Dazu nutzt man super():

class Stock:
  ...
    def cost(self):
        return self.shares * self.price
  ...

class MyStock(Stock):
    def cost(self):
        ## Überprüfe den Aufruf von `super`
        actual_cost = super().cost()
        return 1.25 * actual_cost

Verwende super(), um die vorherige Version aufzurufen.

Hinweis: In Python 2 war die Syntax umständlicher.

actual_cost = super(MyStock, self).cost()

__init__ und Vererbung

Wenn __init__ neu definiert wird, ist es unerlässlich, die Elternklasse zu initialisieren.

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

class MyStock(Stock):
    def __init__(self, name, shares, price, factor):
        ## Überprüfe den Aufruf von `super` und `__init__`
        super().__init__(name, shares, price)
        self.factor = factor

    def cost(self):
        return self.factor * super().cost()

Du solltest die __init__()-Methode auf dem super aufrufen, was der Weg ist, um die vorherige Version aufzurufen, wie zuvor gezeigt.

Verwendung von Vererbung

Vererbung wird manchmal verwendet, um verwandte Objekte zu organisieren.

class Shape:
 ...

class Circle(Shape):
 ...

class Rectangle(Shape):
 ...

Denke an eine logische Hierarchie oder Taxonomie. Ein häufiger (und praktischer) Einsatz ist jedoch in Bezug auf wiederverwendbaren oder erweiterbaren Code. Beispielsweise kann ein Framework eine Basisklasse definieren und Sie auffordern, sie anzupassen.

class CustomHandler(TCPHandler):
    def handle_request(self):
     ...
        ## Anpassende Verarbeitung

Die Basisklasse enthält einige allgemeingültigen Code. Ihre Klasse erbt und passt spezifische Teile an.

"ist ein" - Beziehung

Die Vererbung etabliert eine Typbeziehung.

class Shape:
...

class Circle(Shape):
...

Prüfe auf Objektinstanz.

>>> c = Circle(4.0)
>>> isinstance(c, Shape)
True
>>>

Wichtig: Idealerweise sollte jeder Code, der mit Instanzen der Elternklasse funktioniert, auch mit Instanzen der Kindklasse funktionieren.

Basisklasse object

Wenn eine Klasse keine Elternklasse hat, sieht man manchmal object als Basis verwendet.

class Shape(object):
...

object ist die Elternklasse aller Objekte in Python.

*Hinweis: Technisch gesehen ist dies nicht erforderlich, aber man sieht es oft als Überbleibsel aus der erforderlichen Verwendung in Python 2 angegeben. Wenn es weggelassen wird, erbt die Klasse immer noch implizit von object.

Mehrfachvererbung

Du kannst von mehreren Klassen erben, indem du sie in der Klassendefinition angibst.

class Mother:
...

class Father:
...

class Child(Mother, Father):
...

Die Klasse Child erbt Eigenschaften von beiden Elternklassen. Es gibt einige recht knifflige Details. Mach es nicht, es sei denn, du weißt, was du tust. In der nächsten Abschnitt werden einige weitere Informationen gegeben, aber wir werden in diesem Kurs nicht weiter auf Mehrfachvererbung zurückgreifen.

Ein wichtiger Einsatz von Vererbung besteht darin, Code zu schreiben, der auf verschiedene Weise erweitert oder angepasst werden soll - insbesondere in Bibliotheken oder Frameworks. Um dies zu veranschaulichen, betrachte die Funktion print_report() in deinem Programm report.py. Sie sollte ungefähr so aussehen:

def print_report(reportdata):
    '''
    Druckt eine schön formattierte Tabelle aus einer Liste von (Name, Anteile, Preis, Änderung) - Tupeln.
    '''
    headers = ('Name','Anteile','Preis','Änderung')
    print('%10s %10s %10s %10s' % headers)
    print(('-'*10 +' ')*len(headers))
    for row in reportdata:
        print('%10s %10d %10.2f %10.2f' % row)

Wenn du dein Report-Programm ausführst, solltest du eine Ausgabe wie diese erhalten:

>>> import report
>>> report.portfolio_report('portfolio.csv', 'prices.csv')
      Name     Shares      Price     Change
---------- ---------- ---------- ----------
        AA        100       9.22     -22.98
       IBM         50     106.28      15.18
       CAT        150      35.46     -47.98
      MSFT        200      20.89     -30.34
        GE         95      13.48     -26.89
      MSFT         50      20.89     -44.21
       IBM        100     106.28      35.84

Übung 4.5: Ein Problem mit der Erweiterbarkeit

Angenommen, Sie möchten die Funktion print_report() so ändern, dass sie verschiedene Ausgabeformate wie einfachen Text, HTML, CSV oder XML unterstützt. Um dies zu tun, könnten Sie versuchen, eine riesige Funktion zu schreiben, die alles macht. Dies würde jedoch wahrscheinlich zu einem unhaltbaren Durcheinander führen. Stattdessen ist dies eine perfekte Gelegenheit, die Vererbung zu verwenden.

Beginnen Sie mit den Schritten, die bei der Erstellung einer Tabelle beteiligt sind. Am Anfang der Tabelle befindet sich eine Reihe von Tabellenüberschriften. Danach erscheinen die Zeilen der Tabellendaten. Nehmen wir diese Schritte und bringen Sie sie in ihre eigene Klasse. Erstellen Sie eine Datei namens tableformat.py und definieren Sie die folgende Klasse:

## tableformat.py

class TableFormatter:
    def headings(self, headers):
        '''
        Gibt die Tabellenüberschriften aus.
        '''
        raise NotImplementedError()

    def row(self, rowdata):
        '''
        Gibt eine einzelne Zeile der Tabellendaten aus.
        '''
        raise NotImplementedError()

Diese Klasse tut nichts, aber sie dient als Art von Entwurfspezifikation für zusätzliche Klassen, die bald definiert werden. Eine Klasse wie diese wird manchmal als "abstrakte Basisklasse" bezeichnet.

Ändern Sie die Funktion print_report(), so dass sie ein TableFormatter-Objekt als Eingabe akzeptiert und auf diesem Methoden aufruft, um die Ausgabe zu erzeugen. Beispielsweise wie folgt:

## report.py
...

def print_report(reportdata, formatter):
    '''
    Druckt eine schön formattierte Tabelle aus einer Liste von (Name, Anteile, Preis, Änderung) - Tupeln.
    '''
    formatter.headings(['Name','Anteile','Preis','Änderung'])
    for name, shares, price, change in reportdata:
        rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ]
        formatter.row(rowdata)

Da Sie einem Argument in print_report() hinzugefügt haben, müssen Sie auch die Funktion portfolio_report() ändern. Ändern Sie sie so, dass sie einen TableFormatter wie folgt erstellt:

## report.py

import tableformat

...
def portfolio_report(portfoliofile, pricefile):
    '''
    Erstellt einen Aktienbericht aus den Portfolio - und Preisdaten - Dateien.
    '''
    ## Lese die Daten - Dateien
    portfolio = read_portfolio(portfoliofile)
    prices = read_prices(pricefile)

    ## Erstelle die Berichts - Daten
    report = make_report_data(portfolio, prices)

    ## Drucke sie aus
    formatter = tableformat.TableFormatter()
    print_report(report, formatter)

Führen Sie diesen neuen Code aus:

>>> ================================ RESTART ================================
>>> import report
>>> report.portfolio_report('portfolio.csv', 'prices.csv')
... stürzt ab...

Es sollte sofort mit einer NotImplementedError-Ausnahme abstürzen. Das ist nicht sehr aufregend, aber genau das, was wir erwartet haben. Fortfahren Sie mit dem nächsten Teil.

Übung 4.6: Verwenden von Vererbung, um verschiedene Ausgaben zu erzeugen

Die Klasse TableFormatter, die Sie im Teil (a) definiert haben, soll über die Vererbung erweitert werden. Tatsächlich ist das der ganze Gedanke. Um dies zu veranschaulichen, definieren Sie eine Klasse TextTableFormatter wie folgt:

## tableformat.py
...
class TextTableFormatter(TableFormatter):
    '''
    Gibt eine Tabelle im einfachen - Text - Format aus
    '''
    def headings(self, headers):
        for h in headers:
            print(f'{h:>10s}', end=' ')
        print()
        print(('-'*10 +' ')*len(headers))

    def row(self, rowdata):
        for d in rowdata:
            print(f'{d:>10s}', end=' ')
        print()

Ändern Sie die Funktion portfolio_report() wie folgt und testen Sie sie:

## report.py
...
def portfolio_report(portfoliofile, pricefile):
    '''
    Erstellt einen Aktienbericht aus den Portfolio - und Preisdaten - Dateien.
    '''
    ## Lese die Daten - Dateien
    portfolio = read_portfolio(portfoliofile)
    prices = read_prices(pricefile)

    ## Erstelle die Berichts - Daten
    report = make_report_data(portfolio, prices)

    ## Drucke sie aus
    formatter = tableformat.TextTableFormatter()
    print_report(report, formatter)

Dies sollte die gleiche Ausgabe wie zuvor erzeugen:

>>> ================================ RESTART ================================
>>> import report
>>> report.portfolio_report('portfolio.csv', 'prices.csv')
      Name     Shares      Price     Change
---------- ---------- ---------- ----------
        AA        100       9.22     -22.98
       IBM         50     106.28      15.18
       CAT        150      35.46     -47.98
      MSFT        200      20.89     -30.34
        GE         95      13.48     -26.89
      MSFT         50      20.89     -44.21
       IBM        100     106.28      35.84
>>>

Ändern wir jedoch die Ausgabe in etwas anderes. Definieren Sie eine neue Klasse CSVTableFormatter, die die Ausgabe im CSV - Format erzeugt:

## tableformat.py
...
class CSVTableFormatter(TableFormatter):
    '''
    Gibt Portfolio - Daten im CSV - Format aus.
    '''
    def headings(self, headers):
        print(','.join(headers))

    def row(self, rowdata):
        print(','.join(rowdata))

Ändern Sie Ihr Hauptprogramm wie folgt:

def portfolio_report(portfoliofile, pricefile):
    '''
    Erstellt einen Aktienbericht aus den Portfolio - und Preisdaten - Dateien.
    '''
    ## Lese die Daten - Dateien
    portfolio = read_portfolio(portfoliofile)
    prices = read_prices(pricefile)

    ## Erstelle die Berichts - Daten
    report = make_report_data(portfolio, prices)

    ## Drucke sie aus
    formatter = tableformat.CSVTableFormatter()
    print_report(report, formatter)

Sie sollten jetzt die folgende CSV - Ausgabe sehen:

>>> ================================ RESTART ================================
>>> import report
>>> report.portfolio_report('portfolio.csv', 'prices.csv')
Name,Shares,Price,Change
AA,100,9.22,-22.98
IBM,50,106.28,15.18
CAT,150,35.46,-47.98
MSFT,200,20.89,-30.34
GE,95,13.48,-26.89
MSFT,50,20.89,-44.21
IBM,100,106.28,35.84

Mit einem ähnlichen Ansatz definieren Sie eine Klasse HTMLTableFormatter, die eine Tabelle mit der folgenden Ausgabe erzeugt:

<tr><th>Name</th><th>Shares</th><th>Price</th><th>Change</th></tr>
<tr><td>AA</td><td>100</td><td>9.22</td><td>-22.98</td></tr>
<tr><td>IBM</td><td>50</td><td>106.28</td><td>15.18</td></tr>
<tr><td>CAT</td><td>150</td><td>35.46</td><td>-47.98</td></tr>
<tr><td>MSFT</td><td>200</td><td>20.89</td><td>-30.34</td></tr>
<tr><td>GE</td><td>95</td><td>13.48</td><td>-26.89</td></tr>
<tr><td>MSFT</td><td>50</td><td>20.89</td><td>-44.21</td></tr>
<tr><td>IBM</td><td>100</td><td>106.28</td><td>35.84</td></tr>

Testen Sie Ihren Code, indem Sie das Hauptprogramm ändern, um ein HTMLTableFormatter-Objekt statt eines CSVTableFormatter-Objekts zu erstellen.

✨ Lösung prüfen und üben

Übung 4.7: Polymorphismus in der Anwendung

Ein wichtiges Merkmal der objektorientierten Programmierung ist, dass Sie ein Objekt in ein Programm einfügen können und es funktioniert, ohne dass Sie den vorhandenen Code ändern müssen. Beispielsweise würde ein Programm, das erwartet, ein TableFormatter-Objekt zu verwenden, funktionieren, unabhängig davon, welchem TableFormatter Sie es tatsächlich geben. Dieses Verhalten wird manchmal als "Polymorphismus" bezeichnet.

Ein potenzielles Problem besteht darin, herauszufinden, wie es möglich ist, dass ein Benutzer den Formatter auswählt, den er möchte. Das direkte Verwenden von Klassennamen wie TextTableFormatter ist oft störend. Daher könnten Sie einen vereinfachten Ansatz in Betracht ziehen. Vielleicht integrieren Sie eine if-Anweisung in den Code wie folgt:

def portfolio_report(portfoliofile, pricefile, fmt='txt'):
    '''
    Erstellt einen Aktienbericht aus den Portfolio - und Preisdaten - Dateien.
    '''
    ## Lese die Daten - Dateien
    portfolio = read_portfolio(portfoliofile)
    prices = read_prices(pricefile)

    ## Erstelle die Berichts - Daten
    report = make_report_data(portfolio, prices)

    ## Drucke sie aus
    if fmt == 'txt':
        formatter = tableformat.TextTableFormatter()
    elif fmt == 'csv':
        formatter = tableformat.CSVTableFormatter()
    elif fmt == 'html':
        formatter = tableformat.HTMLTableFormatter()
    else:
        raise RuntimeError(f'Unknown format {fmt}')
    print_report(report, formatter)

In diesem Code gibt der Benutzer einen vereinfachten Namen wie 'txt' oder 'csv' an, um ein Format auszuwählen. Ist es jedoch die beste Idee, eine große if-Anweisung in der Funktion portfolio_report() so zu platzieren? Es wäre vielleicht besser, diesen Code an eine allgemeine Funktion an einem anderen Ort zu verschieben.

In der Datei tableformat.py fügen Sie eine Funktion create_formatter(name) hinzu, die es einem Benutzer ermöglicht, einen Formatter anhand eines Ausgabennamens wie 'txt', 'csv' oder 'html' zu erstellen. Ändern Sie portfolio_report() so, dass es wie folgt aussieht:

def portfolio_report(portfoliofile, pricefile, fmt='txt'):
    '''
    Erstellt einen Aktienbericht aus den Portfolio - und Preisdaten - Dateien.
    '''
    ## Lese die Daten - Dateien
    portfolio = read_portfolio(portfoliofile)
    prices = read_prices(pricefile)

    ## Erstelle die Berichts - Daten
    report = make_report_data(portfolio, prices)

    ## Drucke sie aus
    formatter = tableformat.create_formatter(fmt)
    print_report(report, formatter)

Testen Sie die Funktion mit verschiedenen Formaten, um sicherzustellen, dass sie funktioniert.

✨ Lösung prüfen und üben

Übung 4.8: Alles zusammenbringen

Ändern Sie das Programm report.py so, dass die Funktion portfolio_report() ein optionales Argument erhält, das das Ausgabeformat angibt. Beispielsweise:

>>> report.portfolio_report('portfolio.csv', 'prices.csv', 'txt')
      Name     Shares      Price     Change
---------- ---------- ---------- ----------
        AA        100       9.22     -22.98
       IBM         50     106.28      15.18
       CAT        150      35.46     -47.98
      MSFT        200      20.89     -30.34
        GE         95      13.48     -26.89
      MSFT         50      20.89     -44.21
       IBM        100     106.28      35.84
>>>

Ändern Sie das Hauptprogramm so, dass ein Format über die Befehlszeile angegeben werden kann:

$ python3 report.py portfolio.csv prices.csv csv
Name,Shares,Price,Change
AA,100,9.22,-22.98
IBM,50,106.28,15.18
CAT,150,35.46,-47.98
MSFT,200,20.89,-30.34
GE,95,13.48,-26.89
MSFT,50,20.89,-44.21
IBM,100,106.28,35.84
$
✨ Lösung prüfen und üben

Diskussion

Das Schreiben von erweiterbarem Code ist eine der häufigsten Anwendungen der Vererbung in Bibliotheken und Frameworks. Beispielsweise kann ein Framework Sie dazu anweisen, Ihr eigenes Objekt zu definieren, das von einer bereitgestellten Basisklasse erbt. Anschließend werden Sie aufgefordert, verschiedene Methoden auszufüllen, die verschiedene Funktionalität implementieren.

Ein etwas tiefer liegender Begriff ist die Idee des "Besitzes Ihrer Abstraktionen". In den Übungen haben wir unsere eigene Klasse für die Formatierung einer Tabelle definiert. Sie können sich an Ihren Code sehen und sich sagen: "Ich sollte einfach eine Formatierungsbibliothek oder etwas anderes verwenden, das jemand anderer bereits gemacht hat!" Nein, Sie sollten sowohl Ihre Klasse als auch eine Bibliothek verwenden. Die Verwendung Ihrer eigenen Klasse fördert die lose Kopplung und ist flexibler. Solange Ihre Anwendung die Programmierschnittstelle Ihrer Klasse verwendet, können Sie die interne Implementierung auf jede beliebige Weise ändern, die Sie möchten. Sie können ausschließlich eigenes Code schreiben. Sie können eine Drittanbieter - Paket verwenden. Wenn Sie eines finden, können Sie ein Drittanbieter - Paket gegen ein anderes austauschen. Es spielt keine Rolle - Ihr Anwendungs - Code bricht nicht, solange Sie die Schnittstelle beibehalten. Das ist eine mächtige Idee und einer der Gründe, warum Sie für etwas wie dies die Vererbung in Betracht ziehen sollten.

Allerdings kann das Entwerfen objektorientierter Programme extrem schwierig sein. Für weitere Informationen sollten Sie wahrscheinlich Bücher zum Thema Design Patterns suchen (obwohl das Verständnis dessen, was in dieser Übung passiert ist, Sie weit bringt, was die praktische Verwendung von Objekten betrifft).

Zusammenfassung

Herzlichen Glückwunsch! Sie haben das Vererbungslabor abgeschlossen. Sie können in LabEx weitere Labs ausprobieren, um Ihre Fähigkeiten zu verbessern.