Worum es geht. Klassen in Python — und vor allem die Stellen, an denen sie sich anders verhalten als in Java: kein Zugriffsschutz, Mehrfachvererbung, überladbare Operatoren, und zwei verschiedene Wege, ein Interface zu beschreiben.
Was Sie danach können. self sowie Klassen- und Instanzattribute auseinanderhalten; Operatoren über Dunder-Methoden überladen; entscheiden, ob eine abstrakte Basisklasse oder ein Protocol das richtige Mittel ist; und @property, @dataclass und Enum einsetzen und die Wahl begründen.
Die Java-Entsprechung in einem Satz. Eine ABC ist ein interface mit Vererbungszwang, ein Protocol ist ein interface, das man einer fremden Klasse nachträglich anlegen kann, ohne sie anzufassen — und private gibt es nicht, nur eine Konvention mit Unterstrich.
Eine neue Klasse wird mit class deklariert. Sie muss nicht von einer bestimmten Klasse erben, erbt aber implizit von object:
class c():
pass
assert issubclass(c, object)
print(c, type(c))
# Output: <class '__main__.c'> <class 'type'>
x = c()
assert isinstance(x, c)
assert isinstance(x, object)
print(x, type(x))
# Output: <__main__.c object at 0x7fecd48ecd10> <class '__main__.c'>
Konstruktor und self¶
Wenn man von einer Klasse c eine Instanz anlegen möchte, so behandelt man c wie eine Methode, die eine Instanz zurück gibt. Der Aufruf c() ruft c.__init__() auf. So ein Konstruktor kann auch Argumente nehmen:
class c():
def __init__(self, x : int):
self.value = x
instance = c(23)
print(instance.value)
# Output: 23
Dabei ist das self in der Signatur eine Referenz auf die (neue) Instanz (wie this in Java). Der Aufruf des Konstruktors c(23) erforderte nur x, nicht self. Technisch gesehen ist eigentlich __new__ der Konstruktor und __init__ der Initialisator, aber praktisch schreibt man i.d.R. nur __init__-Methoden. Warum ist das so?
Während man in Java oft viele Konstruktoren mit verschiedenen Methodensignaturen (Typen der Parameter) verwendet, ist das in Python unüblich, denn Methoden werden über ihre Namen identifiziert. Zwei verschiedene Konstruktoren bewirken daher nicht das aus Java erwartete:
class c():
def __init__(self, x : int):
self.value = x
def __init__(self, x : int, y : int):
self.value = x/y
try:
instance = c(23)
except TypeError as e:
print(e)
# Output: c.__init__() missing 1 required positional argument: 'y'
Stattdessen arbeitet man mit flexibler Methodensignatur:
class c():
def __init__(self, x : int, y=None):
if y is None:
self.value = x
else:
self.value = x//y
print(c(42).value, c(121, 23).value)
# Output: 42 5
Und/oder man verwendet ein Factory-Design-Pattern; etwa wenn man eine Datenstruktur aus verschiedenen anderen herstellen kann:
class c():
def __init__(self, n: int):
self.value = n
@staticmethod
def fromPandas(df):
return c(len(df))
@staticmethod
def fromNumpy(arr):
return c(len(arr))
instance = c.fromNumpy([1, 2, 3]) # alles mit len() funktioniert hier (Duck Typing, s.u.)
print(instance.value)
# Output: 3
Konvention statt Schutz¶
Es gibt keine Möglichkeit, Methoden oder Attribute als privat oder anderweitig geschützt zu markieren. Es ist stets möglich, von außen auf alle Methoden und Attribute zuzugreifen. Um zu kommunizieren, was das Interface zur Nutzung einer Klasse und ihrer Instanzen ist, und was nur für die interne Implementierung gedacht war, gibt es die Konvention, einen einzelnen Underscore voranzustellen, um private Attribute und Methoden zu kennzeichnen.
class c():
def _internal(self, x):
return x
def external(self, x):
return self._internal(x)
print(c().external(23))
# Output: 23
#print(c()._internal(23)) # <-- das geht (leider) auchOperatoren überladen¶
Will man z.B. eine Klasse definieren, deren Instanzen sich in irgendeinem Sinne addieren lassen, so kann man dafür die +-Syntax verwenden, indem man die __add__-Methode implementiert:
class c():
def __init__(self, initialValue : int):
self.value = initialValue
x, y = c(2), c(3)
try:
print(x + y)
except TypeError as e:
print(e)
# Output: unsupported operand type(s) for +: 'c' and 'c'class c():
def __init__(self, initialValue : int):
self.value = initialValue
def __add__(self, other):
if isinstance(other, c):
return c(self.value + other.value)
else:
raise TypeError("unsupported operand type(s) for +: 'c' and '%s'" % type(other).__name__)
def __repr__(self):
return "c("+repr(self.value)+")"
x, y, z = c(2), c(3), 4
print(x + y)
# Output: c(5)
try:
print(x + z)
except TypeError as e:
print(e)
# Output: unsupported operand type(s) for +: 'c' and 'int'Super¶
Wenn man eine Klasse von einer anderen erben lässt und eine Methode in der Unterklasse implementiert, die in der Oberklasse bereits definiert war, so muss man selbst explizit die Methode der Oberklasse aufrufen, wenn man das möchte - wie typischerweise in einem Konstruktor:
class s():
def __init__(self, x : int):
self.value = x
class c(s):
def __init__(self, x : int):
super().__init__(x)
self.bigvalue = self.value * 2
Die Methode super geht durch die MRO, die Method Resolution Order. Siehe auch c.__mro__.
Klassen und Instanzen¶
Man kann nicht nur Instanzen mit Eigenschaften und Methoden versehen, sondern auch direkt die Klasse:
class c():
classistvalue = "classy"
def __init__(self, instancevalue):
self.instancevalue = instancevalue
print(c.classistvalue) # Output: classy
c.classistvalue = "very classy"
ins = c("valuable and")
ins.classistvalue = "yet still classy"
print(ins.instancevalue, c.classistvalue) # Output: valuable and very classy
Vor der Zuweisung eines Werts auf ins.classistvalue gibt ins.classistvalue einfach c.classistvalue zurück, danach jedoch eine neue Instanzvariable, sogenanntes Shadowing.
Problematisch wird es, wenn eine Klassenmethode kein self nimmt, aber von einer Instanz aufgerufen wird (sodass dann self übergeben wird). Um klar festzulegen, ob es sich um eine statische Methode handelt, die weder self noch die Klasse als Argument bekommen soll, auch wenn sie von der Instanz aufgerufen wird, verwendet man den @staticmethod-Decorator.
Wenn man anstelle der Instanz als self lieber die Klasse als cls verwendern möchte, ist @classmethod der richtige Decorator:
class c():
@classmethod
def talkClassyNonsense(cls):
print(type(cls), cls)
@staticmethod
def talkNonsense():
print("no sense")
def talkInstance(self):
print(type(self), self)
def talkFree():
print("free sense")
instance = c()
instance.talkClassyNonsense() # Output: <class 'type'> <class '__main__.c'>
instance.talkNonsense() # Output: no sense
instance.talkInstance() # Output: <class '__main__.c'> <__main__.c object at 0x7f01fa8a3650>
instance.talkFree() # Output: TypeError: c.talkFree() takes 0 positional arguments but 1 was given
c.talkClassyNonsense() # Output: <class 'type'> <class '__main__.c'>
c.talkNonsense() # Output: no sense
c.talkInstance() # Output: TypeError: c.talkInstance() missing 1 required positional argument: 'self'
c.talkFree() # Output: free sense
Duck Typing¶
Wenn es läuft wie eine Ente und quakt wie eine Ente, dann ist es eine Ente. Zumindest für die Zwecke von allem, wo es nur um Lauf und Quaken geht. Nach dem Prinzip wird in Python häufig ein bestimmtes Argument nicht von einem bestimmten Typen erwartet, sondern es wird schlicht erwartet, dass bestimmte Methoden oder Eigenschaften implementiert sind (das Quaken und Laufen):
class c():
def quack(self, x):
return 2*x
class d():
def quack(self, y):
return 3*y
def quacker(duck, number):
print(duck.__class__.__name__, "quacks", duck.quack(number))
quacker(c(), 5) # Output: c quacks 10
quacker(d(), 5) # Output: d quacks 15
So lässt sich auch nachträglich das Interface nachrüsten, im Extremfall mit sogenanntem Monkey Patching:
class boringclass():
pass
def quacker(duck, number):
print(duck.__class__.__name__, "quacks", duck.quack(number))
x = boringclass()
y = boringclass()
def cquack(self, x):
return 2*x
boringclass.quack = cquack
quacker(x, 3) # Output: boringclass quacks 6
quacker(y, 3) # Output: boringclass quacks 6
def dquack(y):
return 3*y
y.quack = dquack
quacker(x, 3) # Output: boringclass quacks 6
quacker(y, 3) # Output: boringclass quacks 9
Im vorangegangenen Code-Beispiel sehen wir zwei unterschiedliche Arten: Mit boringclass.quack = cquack haben wir die Klassendeklaration für alle Instanzen von boringclass verändert, also auch von y.
Um nur der Instanz y ein quack zu verleihen, setzen wir y.quack.
ABCs¶
Abstract Base Classes sind ein Zugang, etwas wie Interfaces in Python zu verwenden. Damit lässt sich der Vertrag, den ein Objekt/Parameter/Typ einhalten soll, explizit spezifizieren. Wenn man eine abstrakte Klasse instanziiert, gibt es einen TypeError. Eine Klasse, die von einer abstrakten Klasse erbt und nicht alle abstractmethods implementiert, ist wieder abstrakt.
from abc import ABC, abstractmethod
class planarShape(ABC):
@abstractmethod
def surfaceArea(self):
pass
class squareShape(planarShape):
def __init__(self, sidelength):
self.length = sidelength
def surfaceArea(self):
return self.length ** 2
Am ehesten begegnen uns ABCs in Form der Collections API mit den ABCs Callable, Hashable, Iterable, Iterator, Generator uvm. aber auch im Modul io sowie asyncio kommen ABCs zum Einsatz.
Protocols (strukturelle Typisierung)¶
ABCs beschreiben einen Vertrag nominal: Eine Klasse gehört dazu, weil sie erbt (oder per register nachgemeldet wird). Das setzt voraus, dass man die Klasse in der Hand hat. Genau das ist bei fremdem Code nicht der Fall — und beim Duck Typing oben hatten wir gar keinen aufgeschriebenen Vertrag, sondern nur die Hoffnung, dass quack existiert.
typing.Protocol schließt diese Lücke: Man schreibt auf, welche Methoden ein Objekt haben muss, und ein Type Checker prüft strukturell, ob ein übergebenes Objekt passt. Vererbung ist dafür nicht nötig.
from typing import Protocol
class Quackable(Protocol):
def quack(self, x: int) -> int: ...
class Ente: # erbt *nicht* von Quackable
def quack(self, x: int) -> int:
return 2 * x
class Stein:
pass
def quacker(duck: Quackable, number: int) -> None:
print(duck.__class__.__name__, "quacks", duck.quack(number))
quacker(Ente(), 5) # Output: Ente quacks 10
quacker(Stein(), 5) # läuft auf einen AttributeError zu
Die letzte Zeile scheitert zur Laufzeit mit einem AttributeError — aber Mypy meldet sie schon vorher:
error: Argument 1 to "quacker" has incompatible type "Stein"; expected "Quackable" [arg-type]Das ist formalisiertes Duck Typing: Die drei Punkte ... im Protokoll sind ein echter Funktionskörper (Ellipsis), keine Auslassung im Skript — es gibt hier nichts zu implementieren, nur etwas zu behaupten.
Der praktische Gewinn zeigt sich bei fremdem Code: Man kann das Interface einer Bibliothek nachträglich beschreiben, ohne eine ihrer Zeilen anzufassen. Ein Protokoll lebt bei dem, der es braucht, nicht bei dem, der es erfüllt — man kann also für dieselbe Klasse mehrere schmale Protokolle formulieren, je nachdem, was die aufrufende Funktion wirklich benutzt. In Java entspricht dem am ehesten ein Interface, das man aber zur Definitionszeit der Klasse schon kennen und implements hinschreiben muss.
Braucht man die Prüfung zur Laufzeit, hilft @runtime_checkable:
from typing import Protocol, runtime_checkable
@runtime_checkable
class Quackable(Protocol):
def quack(self, x: int) -> int: ...
print(isinstance(Ente(), Quackable), isinstance(Stein(), Quackable))
# Output: True False
Achtung: isinstance prüft bei Protokollen nur, ob die Namen existieren — Signaturen und Typen werden zur Laufzeit nicht geprüft. Die eigentliche Arbeit macht der Type Checker.
Als Faustregel: ABC, wenn man die Hierarchie selbst baut und gemeinsamen Code vererben will; Protocol, wenn man fremde oder bereits existierende Typen beschreiben will.
Mehrfachvererbung¶
Im Gegensatz zu Java erlaubt Python die Mehrfachvererbung. Eine Klasse kann also von mehreren Elternklassen gleichzeitig erben. Dies wird häufig für sogenannte Mixins verwendet, um einer Klasse bestimmte Fähigkeiten hinzuzufügen, ohne eine tiefe Hierarchie aufzubauen.
Die Reihenfolge der Basisklassen in der Klammer ist dabei entscheidend. Sie bestimmt, in welcher Reihenfolge Python nach Methoden sucht (Method Resolution Order - MRO).
class quackable():
def __init__(self, multiplier):
self.multiplier = multiplier
def quack(self, x):
return self.multiplier * x
class quacklist(list, quackable):
def __init__(self, multiplier):
list.__init__(self)
quackable.__init__(self, multiplier)
def quacker(duck, number):
print(duck.__class__.__name__, "quacks", duck.quack(number))
x = quacklist(3)
x.append(7)
x.append(9)
print(x, type(x)) # Output: [7, 9] <class '__main__.quacklist'>
quacker(x, 5) # Output: quacklist quacks 15
Wenn man in der Klasse quackable z.B. auch __len__ implementiert, wird der Aufruf len(x) trotzdem die list-Implementierung von __len__ verwenden, denn wir haben in der Vererbung list vor quackable aufgeführt und so die method resolution order festgelegt.
Der Property-Decorator¶
Während in Java gern mit Gettern und Settern für Objekteigenschaften gearbeitet wird, um direkt vor/nach dem Setzen und Lesen direkt Code ausführen zu können, ist es in Python üblicher, direkt auf Variablen zuzugreifen. Um dennoch ein Getter/Setter-Pattern zu verwenden, kann man die Variable mit einem vorangestellten Underscore als privat markieren und Getter und Setter implementieren:
class c():
def __init__(self, value=0):
self._value = value
def getValue(self):
print("We give away our value:", self._value)
return self._value
def setValue(self, value):
if value is not None and value > 0:
self._value = value
else:
raise ValueError("We insist on positive values")
x = c()
x.setValue(23)
x.getValue()
# Output: We give away our value: 23
Das lässt sich ein kleines bisschen syntaktisch aufbessern, weil Python den Unterschied zwischen Eigenschaften und Methoden so schön verschwimmen lässt:
class c():
def __init__(self, value=0):
self._value = value
@property
def value(self):
print("We give away our value:", self._value)
return self._value
@value.setter
def value(self, value):
if value is not None and value > 0:
self._value = value
else:
raise ValueError("We insist on positive values")
x = c()
x.value = 23 # calls the setter
print(x.value) # calls the getter
# Output: We give away our value: 23
# 23
Dataclasses (Records)¶
Was der struct in C oder der RECORD in Pascal ist seit Python 3.7 die dataclass. Der Dekorator sorgt dafür, dass __init__ sowie eine lesbare Repräsentation mit __repr__ und der elementweise Vergleich in __eq__ implementiert werden, ohne das explizit selbst machen zu müssen:
from dataclasses import dataclass
@dataclass
class Gegenstand():
name: str
laenge: float
ding = Gegenstand("ding", 5.7)
dang = Gegenstand("zweiter", 23)
dong = Gegenstand(laenge=5.7, name="ding")
assert ding == dong
print(dang)
# Output: Gegenstand(name='zweiter', laenge=23)
Man kann auch explizit mit @dataclass(frozen = True) die Instanzen immutable machen. Das ist nützlich um z.B. Konfigurationsoptionen zu verwalten.
Enums (Aufzählungen)¶
Für eine feste, überschaubare Menge benannter Werte — Produktarten, Spielzustände, Himmelsrichtungen — benutzt man in Python keine Strings und keine int-Konstanten, sondern enum.Enum. Der Grund ist derselbe wie in Java: Ein Tippfehler in "maschinen" fällt erst zur Laufzeit auf (und oft gar nicht), ein Tippfehler in ProductType.MASCHINEN sofort mit einem AttributeError — und der Type Checker sieht ihn schon vorher.
from enum import Enum, auto
class ProductType(Enum):
FOOD = auto()
ENERGY = auto()
MACHINERY = auto()
def __str__(self):
return self.name.title()
print(repr(ProductType.FOOD), ProductType.FOOD.name, ProductType.FOOD.value)
# Output: <ProductType.FOOD: 1> FOOD 1
print(f"Wir handeln mit {ProductType.MACHINERY}.")
# Output: Wir handeln mit Machinery.
auto() vergibt die Werte (hier 1, 2, 3) automatisch — man sollte sich auf sie nur verlassen, wenn sie eine Bedeutung haben. Die Mitglieder sind Singletons, deshalb vergleicht man sie mit is und kann sie als Dictionary-Schlüssel verwenden:
print(ProductType.FOOD is ProductType["FOOD"] is ProductType(1))
# Output: True
lager = {ProductType.FOOD: 12}
print([p.name for p in ProductType], lager[ProductType.FOOD])
# Output: ['FOOD', 'ENERGY', 'MACHINERY'] 12
Eine Enum-Klasse ist also selbst iterierbar, und ProductType["FOOD"] bzw. ProductType(1) sind die beiden Wege zurück — nützlich beim Einlesen von Spielständen oder Konfigurationsdateien. Weil __str__ hier überschrieben ist, erscheint in Ausgaben Machinery statt ProductType.MACHINERY; das ist reine Kosmetik und in der Wirtschaftssimulation genau so gemacht.
Drei Varianten begegnen einem häufig:
IntEnumundStrEnum(3.11) erben zusätzlich vonintbzw.str. Damit lassen sich die Werte direkt serialisieren und mit Zahlen/Strings vergleichen — praktisch an Systemgrenzen (JSON, Datenbank), aber es weicht die Typtrennung wieder auf.Flagfür kombinierbare Bit-Masken (Recht.LESEN | Recht.SCHREIBEN).@enum.uniqueals Dekorator auf der Klasse verbietet Aliase, also zwei Namen mit demselben Wert.
Enums arbeiten gut mit match zusammen und machen eine Verzweigung vollständig überprüfbar:
def beschreibe(p: ProductType) -> str:
match p:
case ProductType.FOOD:
return "verderblich"
case ProductType.ENERGY:
return "sofort zu verbrauchen"
case ProductType.MACHINERY:
return "langlebig"
print(beschreibe(ProductType.ENERGY))
# Output: sofort zu verbrauchen
Mypy weiß, dass die drei Fälle die Enum vollständig abdecken, und akzeptiert die Funktion ohne else. Kommt später ein viertes Produkt hinzu, meldet es genau hier error: Missing return statement [return] — bei Strings als Konstanten wäre der vergessene Fall stillschweigend zu None geworden und erst beim Nutzer aufgefallen.
Zusammenfassung und Kontrollfragen¶
Es gibt keinen Zugriffsschutz: Ein führender Unterstrich ist eine Mitteilung an Lesende, keine Sperre für den Interpreter.
selfist ein gewöhnlicher erster Parameter, und Operatoren sind gewöhnliche Methoden mit festgelegten Namen —a + brufta.__add__(b)auf.Bei Mehrfachvererbung entscheidet die Reihenfolge der Basisklassen über die Method Resolution Order;
super()folgt ihr, und__mro__macht sie sichtbar.Für Interfaces gibt es zwei Wege: ABCs verlangen Vererbung (nominale Typisierung),
Protocolprüft nur die vorhandene Struktur — Letzteres ist das Mittel für fremden Code, den man nicht ändern kann oder darf.@dataclasserzeugt__init__,__repr__und__eq__;Enummacht aus verabredeten Zeichenketten Werte, die man nicht vertippen kann, und erlaubt einem Typechecker, einen vergessenen Fall zu melden.
Kontrollfragen
Sie sollen prüfbar festhalten, welche Methoden Ihr Code von einer Klasse aus einer fremden Bibliothek erwartet — die Bibliothek dürfen Sie nicht ändern. ABC oder
Protocol? Begründen Sie die Wahl.Warum ergibt
c() + c()einenTypeError, solange die Klassecnur__init__definiert? Welche Methode ändert das, und wie heißt sie?Im Kapitel steht
class quacklist(list, quackable). Was würde sich ändern, wenn dortclass quacklist(quackable, list)stünde, und wo lässt sich die daraus folgende Suchreihenfolge ablesen?