Zum Inhalt
wannacode

Interfaces als abstrakte Klassen

Kurz gesagt: Python hat kein interface: Der Vertrag ist eine abstrakte Klasse mit lauter abstrakten Methoden, die jede Klasse schreiben muss, die ihn annimmt.

Problem

Gehälter und Rechnungen

Am Monatsende bezahlt die Firma zwei Arten von Dingen: die Gehälter der Mitarbeiter und die Rechnungen der Lieferanten. Du willst eine Liste und eine Schleife: wer bezahlt wird, wie viel, und am Ende die Summe.

Eine Rechnung ist kein Mitarbeiter. Sie von Employee erben zu lassen ist falsch. Der Satz „eine Rechnung ist ein Mitarbeiter“ stimmt nicht, und die Rechnung bekäme auch einen Namen, ein Gehalt und eine Erhöhung.

Eine gemeinsame Basisklasse Payment mit Feldern und Code ist auch nicht die richtige Antwort: Mitarbeiter und Rechnungen haben keine gemeinsamen Daten und keinen gemeinsamen Code. Gemeinsam ist ihnen nur, dass sie bezahlt werden können.

Die Idee

Ein Interface: ein Vertrag „kann etwas“

Ein Interface ist eine Liste von Methoden ohne Rumpf: was eine Klasse können muss, aber nicht wie. Python hat kein eigenes Wort dafür. Du schreibst den Vertrag als abstrakte Klasse, in der alle Methoden abstrakt sind:

from abc import ABC, abstractmethod
class Payable(ABC):
@abstractmethod
def amount_due(self):
pass
@abstractmethod
def payee(self):
pass
Eine Klasse, die den Vertrag annimmt, implementiert das Interface und schreibt jede seiner Methoden:
class Invoice(Payable):
def __init__(self, supplier, amount):
self._supplier = supplier
self._amount = amount
def amount_due(self):
return self._amount
def payee(self):
return self._supplier
power = Invoice("Stromanbieter", 340)
print(f"{power.payee()}: {power.amount_due()} €")
Ausgabe
Stromanbieter: 340 €

Payable steht in den Klammern wie eine Basisklasse. Python unterscheidet nicht zwischen einer Basisklasse mit Code und einer Vertrags-Basisklasse: Der Unterschied ist, dass Payable keine Felder und keine Rümpfe von Methoden hat. Die Prüfung funktioniert wie bei einer abstrakten Klasse: Von Payable erzeugt man keine Objekte. Payable() bricht ab mit

TypeError: Can't instantiate abstract class Payable without an implementation for abstract methods 'amount_due', 'payee'

Sieh es dir an

Eine Schleife für alles, was bezahlt wird

Auch der Mitarbeiter nimmt den Vertrag an. Sein Betrag ist das Monatsgehalt:

class Employee(Payable):
# ... _name, _salary, __init__, monthly_pay()
def amount_due(self):
return self.monthly_pay()
def payee(self):
return self._name

Jetzt sind sowohl die Mitarbeiter als auch die Rechnungen Payable: Beide Klassen haben es als Basisklasse. Die Liste hat keinen Typ, aber alles darin hält den Vertrag ein, also kann die Schleife bei jedem amount_due() und payee() aufrufen.

payments = [
Employee("Ivan", 2000),
Manager("Maria", 3000, 500),
Invoice("Stromanbieter", 340),
Invoice("Vermieter", 1800),
]
total = 0
for p in payments:
print(f"{p.payee()}: {p.amount_due()} €")
total += p.amount_due()
print(f"Gesamt: {total} €")
Ausgabe
Ivan: 2000 €
Maria: 3500 €
Stromanbieter: 340 €
Vermieter: 1800 €
Gesamt: 7640 €

Der Manager schreibt nichts Neues. Von Employee erbt er sowohl den Vertrag als auch amount_due(). Und amount_due() ruft monthly_pay() auf, das der Manager überschrieben hat. Deshalb bekommt Maria 3500 €.

payments = [
Employee("Ivan", 2000),
Manager("Maria", 3000, 500),
Invoice("Stromanbieter", 340),
]
total = 0
for p in payments:
total += p.amount_due()
print(f"Gesamt: {total} €")
Schritt für Schritt, in Worten
  1. Die Liste enthält drei Objekte aus drei Klassen. Alle sind Payable, und die Pfeile erreichen nur ihren Vertrag.
  2. total beginnt bei 0.
  3. p zeigt auf den Mitarbeiter: amount_due() gibt das Gehalt zurück, 2000.
  4. Der Manager ist ein Mitarbeiter, also kann auch er bezahlt werden. Gehalt plus Bonus: 3500.
  5. Die Rechnung ist kein Mitarbeiter, aber sie hält denselben Vertrag ein: 340.
  6. Die Schleife kennt die Klassen nicht. Sie weiß nur, dass jedes Objekt bezahlt werden kann.

Probier es aus

Wie hoch ist der Betrag?

Was gibt das Programm aus? Die Klassen sind aus der Karte oben.

p = Manager("Maria", 3000, 500)
print(p.amount_due())
Antwort

Die Idee

Eine Basisklasse, viele Verträge

Die Buchhaltung will die Rechnungen auch im CSV-Format: je eine Zeile „Lieferant;Betrag“. Das ist ein zweiter Vertrag. In Python kann eine Klasse beliebig viele Basisklassen haben, durch Kommas getrennt, also sind Verträge einfach weitere Basisklassen:

class Exportable(ABC):
@abstractmethod
def to_csv(self):
pass
class Invoice(Payable, Exportable):
# ... wie vorher
def to_csv(self):
return f"{self._supplier};{self._amount}"
for_accounting = [Invoice("Stromanbieter", 340), Invoice("Vermieter", 1800)]
for e in for_accounting:
print(e.to_csv())
Ausgabe
Stromanbieter;340
Vermieter;1800

Hat die Klasse sowohl eine Basisklasse als auch Verträge, kommt die Basisklasse zuerst:

class Manager(Employee, Exportable):

Der Manager schreibt dann auch to_csv() und ist gleichzeitig ein Employee, ein Payable und ein Exportable. Die Reihenfolge in den Klammern ist wichtig: Wenn zwei Basisklassen eine Methode mit demselben Namen haben, gewinnt die erste.

Basisklassen kann es viele geben, aber am besten hat nur eine Code und Daten. Die übrigen sind Verträge: nur Versprechen, keine Felder.

Probier es aus

Finde den Fehler

Invoice("Stromanbieter", 340) bricht mit TypeError ab. In welcher Zeile ist der Fehler?

Wähle die Zeile mit dem Fehler.

Probier es aus

Was fehlt?

class Invoice():
    # ...

Die Idee

Abstrakte Klasse oder Interface?

In Python sind beide abstrakte Klassen: Ein Vertrag ist eine abstrakte Klasse ohne Felder und ohne Code. Der Unterschied liegt nicht in der Sprache, sondern darin, was sie sagen:

Abstrakte Klasse mit Code Vertrag
Sagt „X ist ein Y“ „X kann“
Wie viele pro Klasse beliebig viele, aber am besten eine beliebig viele
Felder und __init__ ja nein
Gemeinsamer Code ja kaum
  • Eine abstrakte Klasse, wenn die Klassen darunter eine Art von Ding sind und Code und Daten teilen: Figuren haben einen Namen und eine Beschreibung, Mitarbeiter einen Namen und ein Gehalt.
  • Ein Vertrag, wenn die Klassen verschieden sind, aber eine gemeinsame Fähigkeit brauchen: Ein Mitarbeiter und eine Rechnung „können bezahlt werden“, ein Vogel und ein Flugzeug „können fliegen“.
Oft werden sie zusammen benutzt: Employee ist die Basisklasse von Manager und Praktikant und implementiert gleichzeitig Payable.
Genauer, für Neugierige
  • Auch ein Vertrag kann eine Standardmethode haben: eine normale Methode mit Rumpf neben den abstrakten. Die Klassen bekommen sie fertig und können sie auch überschreiben. So kannst du dem Vertrag eine neue Methode hinzufügen, ohne die Klassen kaputt zu machen, die ihn schon erfüllen:
class Flyable(ABC):
@abstractmethod
def fly(self):
pass
def glide(self):
print("Gleitet durch die Luft")
class Bird(Flyable):
def fly(self):
print("Der Vogel schlägt mit den Flügeln")
class Plane(Flyable):
def fly(self):
print("Das Flugzeug hebt ab")
flyers = [Bird(), Plane()]
for f in flyers:
f.fly()
f.glide()
Ausgabe
Der Vogel schlägt mit den Flügeln
Gleitet durch die Luft
Das Flugzeug hebt ab
Gleitet durch die Luft
  • Es gibt einen zweiten Weg, einen Vertrag zu schreiben: typing.Protocol. Die Klasse schreibt den Vertrag dann nicht in ihre Klammern. Solange sie die Methoden hat, passt sie, genau wie in JavaScript. Python selbst prüft das Protokoll nicht: Das Ticket ohne amount_due() geht als Payable durch. Ein eigener Typprüfer prüft es, bevor das Programm läuft. Mit @runtime_checkable und isinstance (die Lektion „Typprüfungen“) kannst du fragen, ob das Objekt die Methoden hat:
from typing import Protocol, runtime_checkable
@runtime_checkable
class Payable(Protocol):
def amount_due(self): ...
def payee(self): ...
class Invoice:
def __init__(self, supplier, amount):
self._supplier = supplier
self._amount = amount
def amount_due(self):
return self._amount
def payee(self):
return self._supplier
class Ticket:
def payee(self):
return "Kino"
p: Payable = Ticket()
print(isinstance(Invoice("Vermieter", 1800), Payable))
print(isinstance(p, Payable))
Ausgabe
True
False

Übung

Verträge

Löse die Aufgaben in deinem Editor. In Python ist jedes „Interface“ eine abstrakte Klasse (ABC) mit lauter abstrakten Methoden.

1. Hupe. Ein Interface Honkable mit einer Methode honk(). SimpleCar aus dem Modul „Klassen und Objekte“ implementiert es.

Ausgabe
Tut-tut!

2. Angaben. Ein Interface Printable mit einer Methode print_details(). Implementiere es in Person, und lass Student (eine Unterklasse von Person) es überschreiben und den Notendurchschnitt hinzufügen. Leg beide in eine Liste.

Ausgabe
Name: Georg, Alter: 45
Name: Elena, Alter: 19
Notendurchschnitt: 5.5

3. Tanz. Ein Interface Danceable mit einer Methode dance(). DanceMove (ein Name und eine Dauer) implementiert es.

Ausgabe
Ich tanze Salsa für 30 Sekunden

4. DJ. Eine Klasse Song mit einem Titel. Ein Interface Playable mit einer Methode play(). DJ implementiert es und hat eine Methode play_song(song), die sich das Lied merkt und es abspielt. Gibt es noch kein Lied, sagt play() das.

Ausgabe
Kein Lied ausgewählt
Ich spiele 'Bohemian Rhapsody'

5. Der Teich. Interfaces Swimmable mit swim() und Flyable mit fly(). Fish schwimmt. Duck schwimmt und fliegt.

Ausgabe
Der Fisch schwimmt.
Die Ente schwimmt.
Die Ente fliegt.

Weitere Aufgaben

  • Rabatt. Ein Interface Discountable mit einer Methode, die den Preis nach dem Rabatt zurückgibt. Implementiere es in Product (10% Rabatt) und in Subscription (25% Rabatt für Studierende). Berechne die Summe eines Warenkorbs mit einem Produkt für 50 € und einem Studentenabo für 12 €: 45 € + 9 € = 54 €.
  • CSV für alle. Lass auch Employee Exportable implementieren, mit einer Zeile „Name;Monatsgehalt“. Leg Mitarbeiter und Rechnungen in eine Liste.

Test

Teste dich

1. Wie viele Verträge (abstrakte Klassen) kann eine Klasse erfüllen?
2. Payable erbt von ABC und hat zwei abstrakte Methoden. Was passiert bei Payable()?
3. Invoice(Payable) schreibt amount_du() statt amount_due(). Wann merkst du das?
4. Wer prüft einen Vertrag, der mit typing.Protocol geschrieben ist?
5. Mitarbeiter und Rechnungen sollen in einer Schleife bezahlt werden. Sie haben keinen gemeinsamen Code. Was passt besser?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·