Typüberprüfung und Schnittstellen

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

In diesem Lab werden Sie Ihre Kenntnisse über Typüberprüfung und Schnittstellen in Python vertiefen. Indem Sie ein Tabellenformatierungsmodul erweitern, werden Sie Konzepte wie abstrakte Basisklassen und Schnittstellenvalidierung implementieren, um robusteres und wartbareres Code zu erstellen.

Dieses Lab baut auf Konzepten aus früheren Übungen auf und konzentriert sich auf Typsicherheit und Entwurfsmuster für Schnittstellen. Ihre Ziele umfassen die Implementierung von Typüberprüfung für Funktionsparameter, das Erstellen und Verwenden von Schnittstellen mit abstrakten Basisklassen sowie die Anwendung des Template-Methoden-Musters zur Reduzierung von Code-Duplizierung. Sie werden tableformat.py, ein Modul zur Formatierung von Daten als Tabellen, und reader.py, ein Modul zum Lesen von CSV-Dateien, ändern.

Dies ist ein Guided Lab, das schrittweise Anweisungen bietet, um Ihnen beim Lernen und Üben zu helfen. Befolgen Sie die Anweisungen sorgfältig, um jeden Schritt abzuschließen und praktische Erfahrungen zu sammeln. Historische Daten zeigen, dass dies ein Labor der Stufe Anfänger mit einer Abschlussquote von 92% ist. Es hat eine positive Bewertungsrate von 90% von den Lernenden erhalten.

In diesem Schritt werden wir die Funktion print_table() in der Datei tableformat.py verbessern. Wir werden eine Überprüfung hinzufügen, um zu sehen, ob der Parameter formatter eine gültige Instanz von TableFormatter ist. Warum brauchen wir das? Nun, Typüberprüfung ist wie ein Sicherheitsnetz für Ihren Code. Sie hilft sicherzustellen, dass die Daten, mit denen Sie arbeiten, vom richtigen Typ sind, was viele schwer zu findende Fehler vermeiden kann.

Verständnis der Typüberprüfung in Python

Typüberprüfung ist eine sehr nützliche Technik in der Programmierung. Sie ermöglicht es Ihnen, Fehler früh im Entwicklungsprozess zu erkennen. In Python arbeiten wir oft mit verschiedenen Objekttypen, und manchmal erwarten wir, dass ein bestimmter Objekttyp an eine Funktion übergeben wird. Um zu überprüfen, ob ein Objekt von einem bestimmten Typ oder einer Unterklasse davon ist, können wir die Funktion isinstance() verwenden. Beispielsweise, wenn Sie eine Funktion haben, die eine Liste erwartet, können Sie isinstance() verwenden, um sicherzustellen, dass die Eingabe tatsächlich eine Liste ist.

Öffnen Sie zunächst die Datei tableformat.py in Ihrem Code-Editor. Scrollen Sie nach unten bis zum Ende der Datei, und Sie finden die Funktion print_table(). So sieht sie zunächst aus:

def print_table(data, columns, formatter):
    '''
    Print a table showing selected columns from a data source
    using the given formatter.
    '''
    formatter.headings(columns)
    for item in data:
        rowdata = [str(getattr(item, col)) for col in columns]
        formatter.row(rowdata)

Diese Funktion nimmt einige Daten, eine Liste von Spalten und einen Formatter entgegen. Anschließend verwendet sie den Formatter, um eine Tabelle auszugeben. Aber im Moment wird nicht überprüft, ob der Formatter vom richtigen Typ ist.

Lassen Sie uns es modifizieren, um die Typüberprüfung hinzuzufügen. Wir verwenden die Funktion isinstance(), um zu überprüfen, ob der Parameter formatter eine Instanz von TableFormatter ist. Wenn dies nicht der Fall ist, werfen wir einen TypeError mit einer klaren Nachricht. Hier ist der modifizierte Code:

def print_table(data, columns, formatter):
    '''
    Print a table showing selected columns from a data source
    using the given formatter.
    '''
    if not isinstance(formatter, TableFormatter):
        raise TypeError("Expected a TableFormatter")

    formatter.headings(columns)
    for item in data:
        rowdata = [str(getattr(item, col)) for col in columns]
        formatter.row(rowdata)

Testen Ihrer Typüberprüfungsimplementierung

Jetzt, da wir die Typüberprüfung hinzugefügt haben, müssen wir sicherstellen, dass sie funktioniert. Lassen Sie uns eine neue Python-Datei namens test_tableformat.py erstellen. Hier ist der Code, den Sie hineinschreiben sollten:

import stock
import reader
import tableformat

## Read portfolio data
portfolio = reader.read_csv_as_instances('portfolio.csv', stock.Stock)

## Define a formatter that doesn't inherit from TableFormatter
class MyFormatter:
    def headings(self, headers):
        pass
    def row(self, rowdata):
        pass

## Try to use the non-compliant formatter
try:
    tableformat.print_table(portfolio, ['name', 'shares', 'price'], MyFormatter())
    print("Test failed - type checking not implemented")
except TypeError as e:
    print(f"Test passed - caught error: {e}")

In diesem Code lesen wir zunächst einige Portfolio-Daten. Dann definieren wir eine neue Formatter-Klasse namens MyFormatter, die nicht von TableFormatter erbt. Wir versuchen, diesen nicht konformen Formatter in der Funktion print_table() zu verwenden. Wenn unsere Typüberprüfung funktioniert, sollte ein TypeError ausgelöst werden.

Um den Test auszuführen, öffnen Sie Ihr Terminal und navigieren Sie zum Verzeichnis, in dem sich die Datei test_tableformat.py befindet. Führen Sie dann den folgenden Befehl aus:

python test_tableformat.py

Wenn alles korrekt funktioniert, sollten Sie eine Ausgabe wie diese sehen:

Test passed - caught error: Expected a TableFormatter

Diese Ausgabe bestätigt, dass unsere Typüberprüfung wie erwartet funktioniert. Jetzt wird die Funktion print_table() nur einen Formatter akzeptieren, der eine Instanz von TableFormatter oder einer seiner Unterklassen ist.

✨ Lösung prüfen und üben

Implementierung einer abstrakten Basisklasse

In diesem Schritt werden wir die TableFormatter-Klasse mithilfe des abc-Moduls in Python in eine richtige abstrakte Basisklasse (ABC) umwandeln. Aber zunächst verstehen wir, was eine abstrakte Basisklasse ist und warum wir sie benötigen.

Verständnis von abstrakten Basisklassen

Eine abstrakte Basisklasse ist ein spezieller Klassen-Typ in Python. Es ist eine Klasse, von der man direkt kein Objekt erstellen kann, das heißt, man kann sie nicht instanziieren. Der Hauptzweck einer abstrakten Basisklasse besteht darin, eine gemeinsame Schnittstelle für ihre Unterklassen zu definieren. Sie legt eine Reihe von Regeln fest, denen alle Unterklassen folgen müssen. Insbesondere erfordert sie, dass Unterklassen bestimmte Methoden implementieren.

Hier sind einige Schlüsselkonzepte zu abstrakten Basisklassen:

  • Wir verwenden das abc-Modul in Python, um abstrakte Basisklassen zu erstellen.
  • Methoden, die mit dem @abstractmethod-Decorator markiert sind, sind wie Regeln. Jede Unterklasse, die von einer abstrakten Basisklasse erbt, muss diese Methoden implementieren.
  • Wenn Sie versuchen, ein Objekt einer Klasse zu erstellen, die von einer abstrakten Basisklasse erbt, aber nicht alle erforderlichen Methoden implementiert hat, wird Python einen Fehler auslösen.

Nachdem Sie die Grundlagen von abstrakten Basisklassen verstanden haben, sehen wir uns an, wie wir die TableFormatter-Klasse so modifizieren können, dass sie eine wird.

Modifizieren der TableFormatter-Klasse

Öffnen Sie die Datei tableformat.py. Wir werden einige Änderungen an der TableFormatter-Klasse vornehmen, damit sie das abc-Modul verwendet und eine abstrakte Basisklasse wird.

  1. Zunächst müssen wir die erforderlichen Elemente aus dem abc-Modul importieren. Fügen Sie die folgende Importanweisung oben in der Datei hinzu:
## tableformat.py
from abc import ABC, abstractmethod

Diese Importanweisung bringt zwei wichtige Dinge mit sich: ABC, die Basisklasse für alle abstrakten Basisklassen in Python, und abstractmethod, ein Decorator, den wir verwenden werden, um Methoden als abstrakt zu markieren.

  1. Als Nächstes werden wir die TableFormatter-Klasse modifizieren. Sie sollte von ABC erben, um eine abstrakte Basisklasse zu werden, und wir werden ihre Methoden mit dem @abstractmethod-Decorator als abstrakt markieren. So sollte die modifizierte Klasse aussehen:
class TableFormatter(ABC):
    @abstractmethod
    def headings(self, headers):
        '''
        Emit the table headings.
        '''
        pass

    @abstractmethod
    def row(self, rowdata):
        '''
        Emit a single row of table data.
        '''
        pass

Beachten Sie einige Dinge an dieser modifizierten Klasse:

  • Die Klasse erbt jetzt von ABC, was bedeutet, dass sie offiziell eine abstrakte Basisklasse ist.
  • Sowohl die headings- als auch die row-Methode sind mit @abstractmethod dekoriert. Dies teilt Python mit, dass jede Unterklasse von TableFormatter diese Methoden implementieren muss.
  • Wir haben den NotImplementedError durch pass ersetzt. Der @abstractmethod-Decorator sorgt dafür, dass Unterklassen diese Methoden implementieren, sodass wir den NotImplementedError nicht mehr benötigen.

Testen Ihrer abstrakten Basisklasse

Nachdem wir die TableFormatter-Klasse zu einer abstrakten Basisklasse gemacht haben, testen wir, ob sie korrekt funktioniert. Wir erstellen eine Datei namens test_abc.py mit dem folgenden Code:

from tableformat import TableFormatter

## Test case 1: Define a class with a misspelled method
try:
    class NewFormatter(TableFormatter):
        def headers(self, headings):  ## Misspelled 'headings'
            pass
        def row(self, rowdata):
            pass

    f = NewFormatter()
    print("Test 1 failed - abstract method enforcement not working")
except TypeError as e:
    print(f"Test 1 passed - caught error: {e}")

## Test case 2: Define a class that properly implements all methods
try:
    class ProperFormatter(TableFormatter):
        def headings(self, headers):
            pass
        def row(self, rowdata):
            pass

    f = ProperFormatter()
    print("Test 2 passed - proper implementation works")
except TypeError as e:
    print(f"Test 2 failed - error: {e}")

In diesem Code haben wir zwei Testfälle. Der erste Testfall definiert eine Klasse NewFormatter, die versucht, von TableFormatter zu erben, aber einen falsch geschriebenen Methodennamen hat. Der zweite Testfall definiert eine Klasse ProperFormatter, die alle erforderlichen Methoden korrekt implementiert.

Um den Test auszuführen, öffnen Sie Ihr Terminal und führen Sie den folgenden Befehl aus:

python test_abc.py

Sie sollten eine Ausgabe ähnlich der folgenden sehen:

Test 1 passed - caught error: Can't instantiate abstract class NewFormatter with abstract methods headings
Test 2 passed - proper implementation works

Diese Ausgabe bestätigt, dass unsere abstrakte Basisklasse wie erwartet funktioniert. Der erste Testfall schlägt fehl, weil die NewFormatter-Klasse die headings-Methode nicht korrekt implementiert hat. Der zweite Testfall schlägt fehl, weil die ProperFormatter-Klasse alle erforderlichen Methoden implementiert hat.

✨ Lösung prüfen und üben

Erstellen von Algorithmus-Template-Klassen

In diesem Schritt werden wir abstrakte Basisklassen nutzen, um ein Template-Methoden-Muster (Template Method Pattern) zu implementieren. Das Ziel besteht darin, die Code-Duplizierung in der CSV-Parsing-Funktionalität zu reduzieren. Code-Duplizierung kann es schwieriger machen, Ihren Code zu warten und zu aktualisieren. Durch die Verwendung des Template-Methoden-Musters können wir eine gemeinsame Struktur für unseren CSV-Parsing-Code erstellen und den Unterklassen die Behandlung der spezifischen Details überlassen.

Verständnis des Template-Methoden-Musters

Das Template-Methoden-Muster ist ein Verhaltensmuster (Behavioral Design Pattern). Es ist wie ein Bauplan für einen Algorithmus. In einer Methode definiert es die Gesamtstruktur oder das "Gerüst" eines Algorithmus. Allerdings implementiert es nicht alle Schritte vollständig. Stattdessen überlässt es einige Schritte den Unterklassen. Dies bedeutet, dass Unterklassen bestimmte Teile des Algorithmus neu definieren können, ohne seine Gesamtstruktur zu ändern.

In unserem Fall, wenn Sie sich die Datei reader.py ansehen, werden Sie feststellen, dass die Funktionen read_csv_as_dicts() und read_csv_as_instances() viel ähnlichen Code haben. Der Hauptunterschied zwischen ihnen besteht darin, wie sie aus den Zeilen der CSV-Datei Datensätze erstellen. Durch die Verwendung des Template-Methoden-Musters können wir vermeiden, den gleichen Code mehrmals zu schreiben.

Hinzufügen der CSVParser-Basisklasse

Beginnen wir damit, eine abstrakte Basisklasse für unser CSV-Parsing hinzuzufügen. Öffnen Sie die Datei reader.py. Wir fügen die abstrakte Basisklasse CSVParser ganz oben in der Datei, direkt nach den Importanweisungen, hinzu.

## reader.py
import csv
from abc import ABC, abstractmethod

class CSVParser(ABC):
    def parse(self, filename):
        records = []
        with open(filename) as f:
            rows = csv.reader(f)
            headers = next(rows)
            for row in rows:
                record = self.make_record(headers, row)
                records.append(record)
        return records

    @abstractmethod
    def make_record(self, headers, row):
        pass

Diese CSVParser-Klasse dient als Template für das CSV-Parsing. Die parse-Methode enthält die gemeinsamen Schritte zum Lesen einer CSV-Datei, wie das Öffnen der Datei, das Abrufen der Überschriften und das Iterieren über die Zeilen. Die spezifische Logik zum Erstellen eines Datensatzes aus einer Zeile wird in die make_record()-Methode abstrahiert. Da es sich um eine abstrakte Methode handelt, muss jede Klasse, die von CSVParser erbt, diese Methode implementieren.

Implementieren der konkreten Parser-Klassen

Nachdem wir nun unsere Basisklasse haben, müssen wir die konkreten Parser-Klassen erstellen. Diese Klassen werden die spezifische Logik zur Datensatz-Erstellung implementieren.

class DictCSVParser(CSVParser):
    def __init__(self, types):
        self.types = types

    def make_record(self, headers, row):
        return { name: func(val) for name, func, val in zip(headers, self.types, row) }

class InstanceCSVParser(CSVParser):
    def __init__(self, cls):
        self.cls = cls

    def make_record(self, headers, row):
        return self.cls.from_row(row)

Die DictCSVParser-Klasse wird verwendet, um Datensätze als Wörterbücher (Dictionaries) zu erstellen. Sie nimmt in ihrem Konstruktor eine Liste von Typen entgegen. Die make_record-Methode verwendet diese Typen, um die Werte in der Zeile zu konvertieren und ein Wörterbuch zu erstellen.

Die InstanceCSVParser-Klasse wird verwendet, um Datensätze als Instanzen einer Klasse zu erstellen. Sie nimmt in ihrem Konstruktor eine Klasse entgegen. Die make_record-Methode ruft die from_row-Methode dieser Klasse auf, um eine Instanz aus der Zeile zu erstellen.

Refactoring der ursprünglichen Funktionen

Jetzt refaktorisieren wir die ursprünglichen Funktionen read_csv_as_dicts() und read_csv_as_instances(), um diese neuen Klassen zu verwenden.

def read_csv_as_dicts(filename, types):
    '''
    Read a CSV file into a list of dictionaries with appropriate type conversion.
    '''
    parser = DictCSVParser(types)
    return parser.parse(filename)

def read_csv_as_instances(filename, cls):
    '''
    Read a CSV file into a list of instances of a class.
    '''
    parser = InstanceCSVParser(cls)
    return parser.parse(filename)

Diese refaktorierten Funktionen haben die gleiche Schnittstelle wie die ursprünglichen. Intern verwenden sie jedoch die neuen Parser-Klassen, die wir gerade erstellt haben. Auf diese Weise haben wir die gemeinsame CSV-Parsing-Logik von der spezifischen Logik zur Datensatz-Erstellung getrennt.

Testen Ihrer Implementierung

Überprüfen wir, ob unser refaktoriertes Code korrekt funktioniert. Erstellen Sie eine Datei namens test_reader.py und fügen Sie den folgenden Code hinzu.

import reader
import stock

## Test the refactored read_csv_as_instances function
portfolio = reader.read_csv_as_instances('portfolio.csv', stock.Stock)
print("First stock:", portfolio[0])

## Test the refactored read_csv_as_dicts function
portfolio_dicts = reader.read_csv_as_dicts('portfolio.csv', [str, int, float])
print("First stock as dict:", portfolio_dicts[0])

## Test direct use of a parser
parser = reader.DictCSVParser([str, int, float])
portfolio_dicts2 = parser.parse('portfolio.csv')
print("First stock from direct parser:", portfolio_dicts2[0])

Um den Test auszuführen, öffnen Sie Ihr Terminal und führen Sie den folgenden Befehl aus:

python test_reader.py

Sie sollten eine Ausgabe ähnlich der folgenden sehen:

First stock: Stock('AA', 100, 32.2)
First stock as dict: {'name': 'AA', 'shares': 100, 'price': 32.2}
First stock from direct parser: {'name': 'AA', 'shares': 100, 'price': 32.2}

Wenn Sie diese Ausgabe sehen, bedeutet dies, dass Ihr refaktoriertes Code korrekt funktioniert. Sowohl die ursprünglichen Funktionen als auch die direkte Verwendung von Parsern liefern die erwarteten Ergebnisse.

✨ Lösung prüfen und üben

Zusammenfassung

In diesem Lab haben Sie mehrere wichtige objektorientierte Programmierungskonzepte gelernt, um Python-Code zu verbessern. Zunächst haben Sie die Typüberprüfung in der Funktion print_table() implementiert, die sicherstellt, dass nur gültige Formatierer verwendet werden, wodurch die Robustheit des Codes verbessert wird. Zweitens haben Sie die Klasse TableFormatter in eine abstrakte Basisklasse umgewandelt, die Unterklassen dazu zwingt, bestimmte Methoden zu implementieren.

Darüber hinaus haben Sie das Template-Methoden-Muster (Template Method Pattern) angewendet, indem Sie die abstrakte Basisklasse CSVParser und ihre konkreten Implementierungen erstellt haben. Dies reduziert die Code-Duplizierung und erhält gleichzeitig eine konsistente Algorithmusstruktur. Diese Techniken sind von entscheidender Bedeutung für die Erstellung von wartbarerem und robusterem Python-Code, insbesondere in großen Anwendungen. Um Ihr Lernen fortzusetzen, erkunden Sie Typ-Hinweise in Python (PEP 484), Protokollklassen und Entwurfsmuster (Design Patterns) in Python.