Aufwärmen · Aufgabe 1 von 7
// A2.3 · ca. 30 Min. · Vertiefung
Strukturelle Typisierung mit Protocol
Nach dieser Lektion beschreiben Sie mit einem Protocol, was ein Objekt können muss, prüfen es zur Laufzeit im Rahmen dieser Prüfung und wählen zwischen Protocol und ABC.
Danach können Sie
- Ein Protocol definieren und als Typ verwenden, den jede Klasse mit passenden Membern erfüllt
- Protocols zur Laufzeit mit @runtime_checkable prüfen und wissen, was diese Prüfung nicht testet
- Zwischen Protocol und ABC wählen: strukturell oder nominal, und was beide zur Laufzeit tun
Vorhersagen · Aufgabe 2 von 7
Sagen Sie es vorher, bevor Sie weiterlesen: File erbt nicht von Closer. Was gibt das aus?
from typing import Protocol class Closer(Protocol): def close(self) -> str: ... class File: def close(self) -> str: return "file closed" def shut(item: Closer) -> str: return item.close() print(shut(File()), Closer in File.__mro__)Üben · Aufgabe 3 von 7
Setzen Sie die Basisklasse ein, die Closer zu einem Protocol macht, sodass jede Klasse mit einer Methode close() dazu passt.
class Closer(____):class Closer():Üben · Aufgabe 4 von 7
mypy lehnt die letzte Zeile ab und ergänzt Hinweise, die Door mit Closer vergleichen. Was zeigt es unter Got:?
from typing import Protocol class Closer(Protocol): def close(self) -> None: ... class Door: def close(self, force: bool) -> None: pass def close_one(item: Closer) -> None: item.close() close_one(Door())Üben · Aufgabe 5 von 7
Ordnen Sie jedem Code zu, was er bewirkt.
Denksport · Aufgabe 6 von 7
Knobelaufgabe. Door.close nimmt ein zusätzliches Argument und liefert einen str. Was gibt das aus?
from typing import Protocol, runtime_checkable @runtime_checkable class Closer(Protocol): def close(self) -> None: ... class Door: def close(self, force: bool) -> str: return "slam" print(isinstance(Door(), Closer))Anwenden · Aufgabe 7 von 7
Mini-Aufgabe. Schreiben Sie ein Protocol Writer mit einer Methode write(self, text: str) -> int und eine Funktion log(target: Writer, message: str), die "[log] ", die Nachricht und einen Zeilenumbruch schreibt. Rufen Sie sie mit einem io.StringIO und mit sys.stdout auf: Keines von beiden erbt von Writer, und mypy --strict soll beide akzeptieren.
Prüfen Sie Ihr Ergebnis anhand dieser Liste
Selbst programmieren
Lesen Sie das ausgearbeitete Beispiel und lösen Sie dann die Übungen. Ihr Code läuft in Ihrem Browser oder auf Ihrem Computer und wird nie hochgeladen.
Ausgearbeitetes Beispiel
Ein Protocol und eine ABC nebeneinander
Closer ist ein zur Laufzeit prüfbares Protocol; Resource ist eine ABC mit abstraktem close() und gemeinsamem describe(). Connection erbt von Resource, bekommt also describe() und muss close() umsetzen. TempFile erbt von nichts, trotzdem akzeptiert close_all beide, weil beide close() haben. Die letzten Zeilen zeigen, welche Laufzeitprüfung welche Beziehung sieht.
main.py
from abc import ABC, abstractmethod
from typing import Protocol, runtime_checkable
@runtime_checkable
class Closer(Protocol):
def close(self) -> None: ...
class Resource(ABC):
"""A family of our own classes: shared code plus a required method."""
def __init__(self, name: str) -> None:
self.name = name
@abstractmethod
def close(self) -> None: ...
def describe(self) -> str:
return f"{type(self).__name__} {self.name}"
class Connection(Resource):
def close(self) -> None:
print("closing", self.describe())
class TempFile: # no base class: it only has the right method
def close(self) -> None:
print("deleting a temporary file")
def close_all(items: list[Closer]) -> None:
for item in items:
item.close()
close_all([Connection("db"), TempFile()])
print(isinstance(TempFile(), Closer), isinstance(TempFile(), Resource))
print(isinstance(Connection("db"), Closer))
Ausführen mit
python main.pyAusgabe
closing Connection db
deleting a temporary file
True False
True- close_all verlangt nur close(), nimmt also Klassen von überall, auch solche, die Sie nicht ändern können.
- TempFile ist ein Closer, aber keine Resource: Die ABC zählt nur Unterklassen.
- Connection ist beides: Es erbt von Resource und hat close(), also passt auch das Protocol.
- Connection("db") funktioniert, weil es das abstrakte close() überschreibt; Resource("db") löste TypeError aus.
Ändern und ausführen
Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.
Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.
Übungen
Übung 1 von 2
Formen, die Sie nicht ändern können
Square und Circle stammen aus einer Bibliothek, die Sie nicht ändern können, und erben nicht von HasArea. Machen Sie HasArea von einer ABC zu einem zur Laufzeit prüfbaren Protocol, damit beide Klassen ohne Änderung zu total_area passen. Schreiben Sie dann only_shapes(items), das mit isinstance() und HasArea die Elemente behält, die eine Methode area() haben.
Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.
Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.
Hinweise
Hinweis 1
Ersetzen Sie ABC durch Protocol und streichen Sie @abstractmethod: Eine Protocol-Methode hat nur ... als Rumpf.
Hinweis 2
Ohne @runtime_checkable löst isinstance(item, HasArea) TypeError aus. Importieren Sie es aus typing und dekorieren Sie die Klasse.
Hinweis 3
only_shapes ist eine List Comprehension: [item for item in items if isinstance(item, HasArea)].
Eine Lösung zeigen
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
import math
from typing import Protocol, runtime_checkable
@runtime_checkable
class HasArea(Protocol):
def area(self) -> float: ...
# From a library: do not change these two classes.
class Square:
def __init__(self, side: float) -> None:
self.side = side
def area(self) -> float:
return self.side**2
class Circle:
def __init__(self, radius: float) -> None:
self.radius = radius
def area(self) -> float:
return math.pi * self.radius**2
def total_area(shapes: list[HasArea]) -> float:
return sum(shape.area() for shape in shapes)
def only_shapes(items: list[object]) -> list[HasArea]:
return [item for item in items if isinstance(item, HasArea)]
Auf dem eigenen Computer ausführen
Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.
main.py
import math
from abc import ABC, abstractmethod
class HasArea(ABC):
@abstractmethod
def area(self) -> float: ...
# From a library: do not change these two classes.
class Square:
def __init__(self, side: float) -> None:
self.side = side
def area(self) -> float:
return self.side**2
class Circle:
def __init__(self, radius: float) -> None:
self.radius = radius
def area(self) -> float:
return math.pi * self.radius**2
def total_area(shapes: list[HasArea]) -> float:
return sum(shape.area() for shape in shapes)
def only_shapes(items: list[object]) -> list[HasArea]:
return []
test_main.py
import math
from typing import is_protocol
from main import Circle, HasArea, Square, only_shapes, total_area
def test_is_protocol():
"""HasArea ist ein Protocol, keine ABC"""
assert is_protocol(HasArea), "HasArea sollte von typing.Protocol erben"
def test_runtime_checkable():
"""isinstance() funktioniert mit HasArea, und Square und Circle passen"""
got = isinstance(Square(2), HasArea), isinstance(Circle(1), HasArea), isinstance("text", HasArea)
assert got == (True, True, False), f"isinstance() ergab {got!r}, erwartet war (True, True, False)"
def test_total_area():
"""total_area addiert 4 für ein Quadrat der Seite 2 und pi für einen Kreis mit Radius 1"""
got = total_area([Square(2), Circle(1)])
assert math.isclose(got, 4 + math.pi), f"total_area lieferte {got!r}, erwartet war {4 + math.pi!r}"
def test_only_shapes():
"""only_shapes behält Quadrat und Kreis und verwirft den Rest"""
square, circle = Square(1), Circle(1)
got = only_shapes([square, "text", 3, circle])
assert got == [square, circle], f"only_shapes lieferte {got!r}, erwartet waren das Quadrat und der Kreis"
Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.
Programm ausführen:
python main.pyPrüfungen ausführen (learnrun.py muss im selben Ordner liegen):
python learnrun.py testlearnrun.py herunterladenÜbung 2 von 2
Ein abstrakter Exporter
Hier sind die Klassen Ihre eigenen und teilen sich export(), also passt eine ABC. Machen Sie Exporter zu einer ABC mit abstraktem render(rows); export(rows) bleibt eine gemeinsame Methode. Exporter() und jede Unterklasse ohne render() müssen dann mit TypeError scheitern. Vervollständigen Sie JsonExporter: render liefert json.dumps(rows).
Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.
Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.
Hinweise
Hinweis 1
from abc import ABC, abstractmethod, dann class Exporter(ABC): und @abstractmethod über render.
Hinweis 2
Eine abstrakte Methode darf einen Rumpf behalten; ... genügt, weil niemand sie direkt aufruft.
Hinweis 3
JsonExporter braucht ein eigenes render(self, rows), das json.dumps(rows) liefert.
Eine Lösung zeigen
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
import json
from abc import ABC, abstractmethod
class Exporter(ABC):
"""Base class for exporters: subclasses must implement render()."""
@abstractmethod
def render(self, rows: list[dict[str, str]]) -> str: ...
def export(self, rows: list[dict[str, str]]) -> str:
if not rows:
return ""
return self.render(rows) + "\n"
class CsvExporter(Exporter):
def render(self, rows: list[dict[str, str]]) -> str:
header = ",".join(rows[0])
lines = [",".join(row.values()) for row in rows]
return "\n".join([header, *lines])
class JsonExporter(Exporter):
def render(self, rows: list[dict[str, str]]) -> str:
return json.dumps(rows)
Auf dem eigenen Computer ausführen
Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.
main.py
import json
class Exporter:
"""Base class for exporters: subclasses must implement render()."""
def render(self, rows: list[dict[str, str]]) -> str:
raise NotImplementedError
def export(self, rows: list[dict[str, str]]) -> str:
if not rows:
return ""
return self.render(rows) + "\n"
class CsvExporter(Exporter):
def render(self, rows: list[dict[str, str]]) -> str:
header = ",".join(rows[0])
lines = [",".join(row.values()) for row in rows]
return "\n".join([header, *lines])
class JsonExporter(Exporter):
pass
test_main.py
import json
from main import CsvExporter, Exporter, JsonExporter
ROWS = [{"name": "Ada", "city": "London"}, {"name": "Alan", "city": "Wilmslow"}]
def test_base_is_abstract():
"""Exporter() löst TypeError aus"""
try:
Exporter()
except TypeError:
return
assert False, "Exporter() hat ein Objekt erzeugt; machen Sie render zu einer @abstractmethod einer ABC"
def test_missing_render():
"""Eine Unterklasse ohne render() lässt sich nicht instanziieren"""
class Broken(Exporter):
pass
try:
Broken()
except TypeError:
return
assert False, "eine Unterklasse ohne render() wurde instanziiert"
def test_csv():
"""CsvExporter schreibt eine Kopfzeile und eine Zeile pro Datensatz"""
got = CsvExporter().export(ROWS)
assert got == "name,city\nAda,London\nAlan,Wilmslow\n", f"CsvExporter().export lieferte {got!r}"
def test_json():
"""JsonExporter schreibt die Datensätze als JSON, plus Zeilenumbruch"""
got = JsonExporter().export(ROWS)
assert got == json.dumps(ROWS) + "\n", f"JsonExporter().export lieferte {got!r}"
def test_empty():
"""export([]) liefert einen leeren String"""
got = JsonExporter().export([])
assert got == "", f"export([]) lieferte {got!r}, erwartet war ''"
Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.
Programm ausführen:
python main.pyPrüfungen ausführen (learnrun.py muss im selben Ordner liegen):
python learnrun.py testlearnrun.py herunterladenHäufige Fehler
isinstance() mit einem einfachen Protocol
from typing import Protocol
class Closer(Protocol):
def close(self) -> None: ...
class File:
def close(self) -> None:
pass
print(isinstance(File(), Closer))
Was Python ausgibt
TypeError: Instance and class checks can only be used with @runtime_checkable protocolsWarum, und die Lösung
Ein Protocol ist standardmäßig für den Typprüfer da. Um es zur Laufzeit zu testen, dekorieren Sie es: from typing import runtime_checkable, dann @runtime_checkable über class Closer(Protocol):. Denken Sie daran, dass die Prüfung dann nur testet, ob close existiert, nicht seine Signatur. mypy meldet die ursprüngliche Zeile ebenfalls: Only @runtime_checkable protocols can be used with instance and class checks [misc].
Ein Protocol instanziieren
from typing import Protocol
class Closer(Protocol):
def close(self) -> None: ...
item = Closer()
Was Python ausgibt
TypeError: Protocols cannot be instantiatedWarum, und die Lösung
Ein Protocol beschreibt Objekte, es ist selbst keins. Erzeugen Sie ein Objekt einer Klasse, die die Member hat, und annotieren Sie mit dem Protocol: item: Closer = File(). Wollen Sie gemeinsame Standard-Methoden, kann eine Klasse ausdrücklich vom Protocol erben und ist dann instanziierbar, oder Sie nehmen eine ABC.
Eine abstrakte Methode in der Unterklasse vergessen
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float: ...
class Square(Shape):
def __init__(self, side: float) -> None:
self.side = side
Square(2)
Was Python ausgibt
TypeError: Can't instantiate abstract class Square without an implementation for abstract method 'area'Warum, und die Lösung
Eine ABC verweigert ein Objekt, solange eine @abstractmethod nicht überschrieben ist. Das ist ihre Garantie zur Laufzeit, und der Fehler nennt die fehlende Methode. Ergänzen Sie in Square def area(self) -> float: return self.side ** 2. mypy meldet dasselbe vor dem Ausführen: Cannot instantiate abstract class "Square" with abstract attribute "area".
Python im Browser: Pyodide 314.0.7, MPL-2.0. Lizenz und Quellcode
Abschlussquiz
5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.
Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.