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 fromPandas(df):
return c(len(df))
def fromNumpy(arr):
return c(len(arr))
instance = c.fromPandas(df)
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.
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.