10 Fortgeschrittene Objektorientierung

1 type(), isinstance() und issubclass()

Diese eingebauten Funktionen helfen beim Umgang mit Typen und Vererbung.

  • type(obj) gibt den exakten Typ des Objekts zurück.
  • isinstance(obj, cls) prüft, ob obj eine Instanz von cls oder einer abgeleiteten Klasse ist.
  • issubclass(sub, super) prüft, ob sub eine Unterklasse von super ist.
class Animal: pass
class Dog(Animal): pass

a = Animal()
d = Dog()

print(type(d))                  # <class '__main__.Dog'>
print(isinstance(d, Animal))    # True
print(issubclass(Dog, Animal))  # True

Diese Funktionen sind wichtig für dynamisches Verhalten, Validierung und Typprüfung.

2 __init__ vs __new__

  • __new__ ist für das Erzeugen eines neuen Objekts zuständig.
  • __init__ wird anschließend aufgerufen, um das Objekt zu initialisieren.
class Custom:
    def __new__(cls, *args, **kwargs):
        print('Creating instance')
        return super().__new__(cls)

    def __init__(self, value):
        print('Initializing with', value)
        self.value = value

obj = Custom(42)

__new__ wird z. B. bei unveränderlichen Typen wie str oder tuple benötigt, wenn diese beeinflusst werden sollen oder in der Metaprogrammierung (Singleton-Pattern).

3 Methodenarten

3.1 StaticMethods und ClassMethods

  • @staticmethod definiert eine Methode, die keinen Zugriff auf self oder cls benötigt.
  • @classmethod arbeitet mit cls und kann so auf die Klasse zugreifen.
class Example:
    @staticmethod
    def utility():
        print('Static method called')

    @classmethod
    def construct(cls):
        print(f'Creating instance of {cls.__name__}')
        return cls()

Example.utility()
obj = Example.construct()

classmethod wird häufig für alternative Konstruktoren verwendet.

3.2 Properties (Private Attribute)

Mit @property können Methoden wie Attribute verwendet werden. Das ist nützlich für gekapselte Attribute, z. B. mit Validierung oder automatischer Berechnung.

class Person:
    def __init__(self, name):
        self._name = name  # Private Konvention

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, new):
        self._name = new

p = Person('Alice')
print(p.name)
p.name = 'Bob'
print(p.name)

Durch Properties kann die API einfach bleiben, während intern komplexe Logik stattfinden kann.

4 Dunder Methods

Dunder (Double Underscore) Methoden ermöglichen benutzerdefiniertes Verhalten für Operatoren und eingebaute Funktionen (__str__, __len__, __getitem__ usw.).

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __str__(self):
        return f'({self.x}, {self.y})'

    def __eq__(self, other):
        return self.x == other.x and self.y == other.y

p1 = Point(1, 2)
p2 = Point(1, 2)
print(str(p1))  # (1, 2)
print(p1 == p2)  # True

Diese Methoden machen Objekte "pythonisch".

5 Abstraktion und Vererbung

5.1 Abstract Methods

Das abc-Modul erlaubt die Definition abstrakter Basisklassen. Methoden mit @abstractmethod müssen in Subklassen implementiert werden.

from abc import ABC, abstractmethod

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

class Circle(Shape):
    def area(self):
        return 3.14 * 5 * 5

Abstrakte Methoden zwingen Unterklassen zur Implementierung und helfen beim Design stabiler APIs.

5.2 Method Resolution Order

Python verwendet das C3 Linearization-Verfahren, um die Reihenfolge von Vererbungen zu bestimmen. Die mro()-Methode zeigt die Aufrufreihenfolge.

class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass

print(D.mro())  # [D, B, C, A, object]

Wichtig bei Mehrfachvererbung.

6 Interation und Indizierung

6.1 Iterator-Klasse und Generator-Funktion

Ein Iterator benötigt die Methoden __iter__ und __next__. Alternativ kann yield verwendet werden.

class Count:
    def __init__(self, max):
        self.max = max
        self.current = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.current >= self.max:
            raise StopIteration
        self.current += 1
        return self.current

Generatoren vereinfachen Iteration:

def count_up_to(max):
    current = 0
    while current < max:
        current += 1
        yield current

6.2 Indexing-Klasse

Mithilfe von __getitem__ und __setitem__ können Objekte wie Listen verwendet werden.

class CustomList:
    def __init__(self, data):
        self.data = data

    def __getitem__(self, index):
        return self.data[index]

    def __setitem__(self, index, value):
        self.data[index] = value

Sehr nützlich für eigene Containerklassen.

7 Datenpräsentation

7.1 Dataclass

Dataclasses automatisieren Konstruktor, Vergleich und Darstellung.

from dataclasses import dataclass

@dataclass
class User:
    name: str
    age: int

7.1.1 Slots

Mit __slots__ wird der Speicherverbrauch reduziert und der Zugriff beschleunigt.

class Slim:
    __slots__ = ('x', 'y')
    def __init__(self, x, y):
        self.x = x
        self.y = y

Seit Python 3.10 ist folgendes möglich (gleichwertiger Code):

@dataclass(slots=True)
class Slim:
    x: int
    y: int
Was macht __slots__ bzw. slots=True?

Normalerweise speichert Python Objektattribute in einem internen Dictionary namens __dict__. Das ist flexibel, jedoch nicht speichereffizient oder performant.
Wenn __slots__ bzw. slots=True verwendet wird, wird dieses Dictionary ersetzt durch ein festeres Layout, bei dem nur die im Slot definierten Felder erlaubt sind.

Warum verbessert das die Leistung?

  1. Weniger Speicherverbrauch:
    Jedes Objekt braucht weniger Speicher, weil kein __dict__ mehr angelegt wird.

  2. Schnellerer Zugriff auf Attribute:
    Statt eines Dictionary-Lookups wird ein schnellerer, indexbasierter Zugriff verwendet.

  3. Bessere Caching-Effekte:
    Schlankere Objekte passen besser in den Cache, was zu weiteren Geschwindigkeitsgewinnen führen kann – besonders bei großen Listen von Objekten.

7.1.2 frozen=True

Das frozen=True-Argument macht die Instanz unveränderlich (immutable):

@dataclass(frozen=True)
class Point:
    x: int
    y: int

p = Point(1, 2)
print(p.x)  # -> 1

p.x = 10    # -> Fehler: cannot assign to field 'x'
  • Man kann keine Attribute mehr ändern nach der Erstellung der Instanz.
  • Python generiert automatisch einen setattr, der Änderungen verhindert.
  • Die Klasse wird hashbar (vorausgesetzt alle Felder sind auch hashbar), was z. B. erlaubt, sie als Schlüssel in einem dict oder als Elemente in einem set zu verwenden.
  • Die Verwendung von frozen=True führt zu einer Leistungssteigerung, diese ist jedoch minimal.

7.2 Namedtuple

Ein namedtuple ist ein leichtgewichtiger, unveränderlicher Datenträger mit benannten Feldern.

from collections import namedtuple

Point = namedtuple('Point', ['x', 'y'])
p = Point(3, 4)
print(p.x, p.y)

Effizient und leserlich – eine gute Alternative zu kleinen Klassen.

8 Enum

Enum erlaubt es, symbolische Konstanten mit Namen zu versehen.

from enum import Enum

class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3

Gleichbedeutend mit dem obigen Beispiel ist:

from enum import Enum

class Color(Enum):
    RED = auto()
    GREEN = auto()
    BLUE = auto()

Enums verbessern die Lesbarkeit und Typsicherheit im Code.

9 Class Decorator

Class Decorators modifizieren Klassen beim Erzeugen. Sie eignen sich für Registrierung, Debugging oder Vererbung.

def debug(cls):
    original_init = cls.__init__
    def new_init(self, *args, **kwargs):
        print(f'Creating {cls.__name__} with {args}, {kwargs}')
        original_init(self, *args, **kwargs)
    cls.__init__ = new_init
    return cls

@debug
class Product:
    def __init__(self, name):
        self.name = name

p = Product('Book')

Class Decorators sind ein mächtiges Meta-Programmierungstool.

10 Zusammenfassung

  1. Typprüfung und Instanzen

    • Mit type(), isinstance() und issubclass() prüft man Objekttypen und Vererbungsbeziehungen.
  2. Objekt-Erzeugung

    • __new__ erzeugt das Objekt (besonders bei Immutable-Typen wichtig).
    • __init__ initialisiert das Objekt nach der Erzeugung.
  3. Methodenarten

    • @staticmethod: Kein Zugriff auf Klassen- oder Instanzdaten.
    • @classmethod: Zugriff auf die Klasse (cls).
    • @property: Ermöglicht kontrollierten Zugriff auf Attribute wie bei einem Feld.
  4. Spezialmethoden (Dunder Methods)

    • Methoden wie __str__, __eq__, __getitem__ erlauben es, benutzerdefinierte Objekte wie eingebaute Typen zu behandeln.
  5. Abstraktion und Vererbung

    • Abstrakte Klassen (ABC, @abstractmethod) erzwingen Implementierungen in Unterklassen.
    • Die Method Resolution Order (MRO) bestimmt die Aufrufreihenfolge bei Mehrfachvererbung.
  6. Iteration und Indexierung

    • Iterator-Klassen und Generator-Funktionen ermöglichen eigene Iterationslogik.
    • Mit __getitem__ und __setitem__ lassen sich Objekte wie Listen verwenden.
  7. Speicher- und Datenrepräsentation

    • @dataclass reduziert Boilerplate für Datenobjekte.
      • __slots__ spart Speicher durch festen Attributsatz.
      • frozen=True macht die Dataclass unveränderbar.
    • namedtuple ist eine kompakte, unveränderliche Datenstruktur mit Feldnamen.
  8. Enumerationen

    • Mit Enum kann man symbolische Konstanten definieren, die lesbar und typsicher sind.
  9. Klassen-Dekoratoren

    • Decorators für Klassen können beim Erzeugen einer Klasse deren Verhalten ändern oder erweitern – ideal für Logging, Validierung oder automatische Registrierung.