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.
Hinzufügen von Typüberprüfung zu print_table()
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.
Modifizieren der print_table()-Funktion
Ö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.
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.
- 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.
- Als Nächstes werden wir die
TableFormatter-Klasse modifizieren. Sie sollte vonABCerben, 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 dierow-Methode sind mit@abstractmethoddekoriert. Dies teilt Python mit, dass jede Unterklasse vonTableFormatterdiese Methoden implementieren muss. - Wir haben den
NotImplementedErrordurchpassersetzt. Der@abstractmethod-Decorator sorgt dafür, dass Unterklassen diese Methoden implementieren, sodass wir denNotImplementedErrornicht 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.
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.
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.