Zum Inhalt
wannacode

Abstrakte Klassen

Kurz gesagt: Eine abstrakte Klasse erbt in Python von ABC und kann keine Objekte haben, und die Methoden mit @abstractmethod muss jede Unterklasse schreiben.

Problem

Null Quadratmeter

Du schreibst ein Programm, das Fliesen für eine Terrasse bestellt. Die Terrasse hat drei Teile: ein Rechteck von 4 × 3 m, einen runden Teil mit 1 m Radius und ein Dreieck mit 2 m Grundseite und 3 m Höhe. Es gibt eine Basisklasse Shape mit area(), die 0 zurückgibt, und jede Figur überschreibt sie.

Der Kollege, der Triangle geschrieben hat, hat es vergessen:

class Triangle(Shape):
def __init__(self, base, height):
self._base = base
self._height = height
terrace = [Rectangle(4, 3), Circle(1), Triangle(2, 3)]
total = 0
for s in terrace:
total += s.area()
print(f"Fliesen: {total:.2f} m²")
Ausgabe
Fliesen: 15.14 m²

Du brauchst 18.14 m². Das Programm meldet keinen Fehler, und der Bestellung fehlen 3 m². Die Null der Basisklasse hat die fehlende Methode versteckt.

Und was ist Shape()? Eine Figur ohne Form. So ein Objekt hat keinen Grund zu existieren.

Die Idee

Abstrakte Klasse: ein Versprechen, das die Unterklassen halten

Shape ist keine echte Figur. Sie sagt nur „jede Figur hat eine Fläche“. Also mach sie abstrakt:

from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
  • ABC (Abstract Base Class) und abstractmethod kommen aus dem Modul abc, deshalb ist die erste Zeile from abc import ABC, abstractmethod.
  • Eine abstrakte Methode hat @abstractmethod darüber. Ihr Rumpf ist nur pass, „nichts“. Einer „Figur im Allgemeinen“ kannst du keine sinnvolle Fläche geben, also gibst du keine.
  • Eine abstrakte Klasse erbt von ABC und kann keine Objekte haben: Shape() bricht das Programm mit TypeError ab.
  • Jede Unterklasse muss die Methode schreiben. Sonst kann man auch von ihr keine Objekte erzeugen.

Python hat keinen Compiler, der das Programm im Voraus durchsieht. Die Prüfung passiert, wenn das Objekt erzeugt wird. Jetzt hält das vergessene Triangle das Programm genau in der Zeile an, die die Terrasse baut, bevor eine falsche Bestellung berechnet wird:

„TypeError: Can't instantiate abstract class Triangle without an implementation for abstract method 'area'“.

Ein Programm, das mit einem klaren Fehler anhält, ist besser als ein stiller Fehler in der Bestellung.
Genauer, für Neugierige
  • Eine Unterklasse, die die abstrakte Methode nicht schreibt, bleibt von selbst abstrakt, ohne dass du es sagst. Das Versprechen geht an ihre eigenen Unterklassen weiter.
  • Eine Unterklasse von ABC ohne eine einzige abstrakte Methode ist nicht abstrakt: Von ihr kann man Objekte erzeugen. Das Verbot kommt von den abstrakten Methoden.
  • @abstractmethod ohne ABC hält nichts auf: Shape() wird erzeugt, und area() gibt None zurück.

Sieh es dir an

Das Dreieck hält das Versprechen

Füg die Methode zu Triangle hinzu:

class Triangle(Shape):
# ... __init__ wie vorher
def area(self):
return self._base * self._height / 2
terrace = [Rectangle(4, 3), Circle(1), Triangle(2, 3)]
total = 0
for s in terrace:
total += s.area()
print(f"Fliesen: {total:.2f} m²")
Ausgabe
Fliesen: 18.14 m²

Die Schleife ist dieselbe. Die Liste enthält Figuren aus drei Klassen, und s.area() ruft die Methode jeder einzelnen auf. Die Polymorphie aus der letzten Lektion funktioniert auch mit einer abstrakten Basisklasse.

Probier es aus

Finde den Fehler

Das Programm bricht in einer der Zeilen ab. In welcher?

Wähle die Zeile mit dem Fehler.

Die Idee

Abstrakt, aber nicht leer

Eine abstrakte Klasse kann alles haben, was eine normale hat: Felder, einen Konstruktor, normale Methoden. Jede Figur soll einen Namen und eine Methode haben, die sie beschreibt:

class Shape(ABC):
def __init__(self, name):
self._name = name
@abstractmethod
def area(self):
pass
def describe(self):
return f"{self._name}: {self.area():.2f} m²"

describe() wird einmal geschrieben, in Shape. Sie ruft area() auf, die in Shape nur ein Versprechen ist. Das ist kein Problem: describe() wird immer auf einer echten Figur aufgerufen, und eine echte Figur hat eine Fläche.

Von Shape erzeugst du keine Objekte, aber ihr __init__ ist nützlich: Die Unterklassen rufen ihn auf.

class Circle(Shape):
def __init__(self, radius):
super().__init__("Kreis")
self._radius = radius
# ... area() wie vorher
terrace = [Rectangle(4, 3), Circle(1), Triangle(2, 3)]
for s in terrace:
print(s.describe())
Ausgabe
Rechteck: 12.00 m²
Kreis: 3.14 m²
Dreieck: 3.00 m²

Die Basisklasse schreibt das Gemeinsame (den Namen, die Beschreibung), und die Unterklassen schreiben das Unterschiedliche (die Fläche).

@abstractmethod
def area(self):
pass
def describe(self):
return f"{self._name}: {self.area():.2f} m²"
s = Circle(1)
print(s.describe())
Schritt für Schritt, in Worten
  1. Shape verspricht area(): @abstractmethod, und der Rumpf ist nur pass. Es kann kein Objekt geben, das nur eine Shape ist.
  2. Das Objekt ist ein Kreis: Der Teil von Circle hat sein eigenes area(). Er hält das Versprechen.
  3. s.describe() ist in Shape geschrieben und funktioniert für jede Figur.
  4. Sie ruft area() auf. Das Objekt ist ein Kreis, also läuft die Methode von Circle: π · 1 · 1.
  5. describe() setzt den Text zusammen. Das Gemeinsame steht in der Basisklasse, das Unterschiedliche in der Unterklasse.
  6. Die Ausgabe ist die Fläche des Kreises, mit zwei Nachkommastellen.

Probier es aus

Wessen Methode?

Was gibt das Programm aus?

# Shape und Circle sind aus der Karte oben
s = Circle(2)
print(s.describe())
Antwort

Probier es aus

Was fehlt?

Was muss in die Lücke, damit man kein Objekt von Shape erzeugen kann?

class Shape():
    @abstractmethod
    def area(self):
        pass

Übung

Figuren und Tiere

Löse die Aufgaben in deinem Editor.

1. Fläche und Umfang. Eine abstrakte Klasse Shape mit einem Namen und zwei abstrakten Methoden: area() und perimeter(). Unterklassen Circle (Radius) und Rectangle (Breite und Höhe). Leg einen Kreis mit Radius 5 und ein Rechteck von 4 × 2.5 in eine Liste und gib beide Werte mit zwei Nachkommastellen aus.

Ausgabe
Kreis: Fläche 78.54, Umfang 31.42
Rechteck: Fläche 10.00, Umfang 13.00

2. Tiere. Eine abstrakte Klasse Animal mit einer abstrakten Methode sound() und einer normalen Methode breathe(), die „Das Tier atmet.“ ausgibt. Unterklassen Cat und Dog. Ruf in einer Schleife den Laut beider auf, dann breathe() auf dem ersten.

Ausgabe
Miau, miau!
Wau, wau!
Das Tier atmet.

Übung

Fahrzeuge und Auftritte

3. Fahrzeuge. Eine abstrakte Klasse Vehicle mit einer abstrakten Methode move(). SimpleCar und Bicycle erben von ihr. Bicycle hat außerdem eine eigene Methode pedal().

Ausgabe
Das Auto fährt los
Das Fahrrad fährt los
Ich trete in die Pedale
Versuch garage[1].pedal(): Es funktioniert, weil dort ein Fahrrad steht. Und garage[0].pedal()? Warum bricht das Programm erst in dieser Zeile ab und nicht früher?

4. Auftritte. Eine abstrakte Klasse Performance mit einem Titel und einer abstrakten Methode start(). DanceMove und Song erben von ihr. Song hat außerdem eine Methode sing().

Ausgabe
Es beginnt der Tanz moonwalk
Es beginnt das Lied 'Bohemian Rhapsody'
Ich singe 'Bohemian Rhapsody'

Weitere Aufgaben

  • Dreieck. Füg Aufgabe 1 ein Triangle mit seinen drei Seiten hinzu. Der Umfang ist ihre Summe, und die Fläche kommt aus der Formel von Heron: s = Umfang / 2, Fläche = √(s(s − a)(s − b)(s − c)). Bei den Seiten 3, 4 und 5 ist die Fläche 6.00 und der Umfang 12.00.
  • Abstrakter Mitarbeiter. Mach Employee aus der letzten Lektion abstrakt, mit einem abstrakten monthly_pay(). Füg FullTime hinzu (das Gehalt wird voll ausgezahlt) und behalte Manager und Intern. Was gewinnt das Programm? Was verliert es?

Test

Teste dich

1. Shape erbt von ABC und hat ein abstraktes area(). Was funktioniert?
2. Was sagt @abstractmethod über einer Methode?
3. Triangle erbt von der abstrakten Shape, schreibt aber area() nicht. Was passiert?
4. Kann eine abstrakte Klasse __init__, Felder und normale Methoden haben?

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