Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Objektorientierte Programmierung

Heinrich-Heine Universität Düsseldorf

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) auch

Operatoren ü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.