Skip to content

Objektorientierte Programmierung in Python

Die Grundlagen der Objektorientierung verstehen und Dataclasses für strukturierte Daten nutzen.

Viele Python-Skripte kommen vollständig ohne Objektorientierung aus. Für größere Projekte oder komplexere Datenstrukturen kann sie jedoch helfen, Code übersichtlicher und besser wartbar zu gestalten.

Im Administrationsalltag werden Klassen häufig verwendet, um Geräte, Benutzer, Konfigurationsobjekte, API-Antworten oder andere zusammengehörige Daten abzubilden.

Besonders wichtig sind Dataclasses. Sie ermöglichen es, strukturierte Daten mit minimalem Aufwand zu modellieren und gehören heute zu den am häufigsten verwendeten Sprachfeatures in diesem Bereich.

Dieses Kapitel vermittelt die wichtigsten Grundlagen, ohne tief in fortgeschrittene OOP-Konzepte einzusteigen.

Klassen und Objekte

Klassen verstehen: Warum und wann?

In der Systemadministration und Infrastrukturautomatisierung begegnen wir oft zusammengehörigen Daten und Funktionen. Beispielsweise gehören zu einem Server nicht nur dessen IP-Adresse und Hostname, sondern auch Methoden, um den Server zu starten, zu stoppen oder den Status abzufragen. Hier helfen Klassen, diese Daten und Funktionen sinnvoll zu bündeln.

Im Vergleich zu einfachen Funktionen oder Dictionaries bieten Klassen eine klare Struktur: Sie fassen Daten (Attribute) und Verhalten (Methoden) zusammen. So bleibt Ihr Code übersichtlich und leichter wartbar, besonders bei komplexeren Automatisierungsaufgaben.

Eine einfache Klasse definieren

Schauen wir uns eine praktische Klasse an, die einen Server in einem Netzwerk repräsentiert:

class Server:
    def __init__(self, hostname, ip):
        self.hostname = hostname  # Hostname des Servers
        self.ip = ip              # IP-Adresse
        self.online = False       # Status: online oder offline

    def start(self):
        # Simuliere Serverstart
        self.online = True
        print(f"Server {self.hostname} gestartet.")

    def stop(self):
        # Simuliere Serverstopp
        self.online = False
        print(f"Server {self.hostname} gestoppt.")

    def status(self):
        return "online" if self.online else "offline"


# Beispiel: Serverobjekt erstellen und Methoden nutzen
mein_server = Server("web01", "192.168.1.10")
print(mein_server.status())  # offline
mein_server.start()
print(mein_server.status())  # online
mein_server.stop()

In diesem Beispiel sehen Sie, wie Daten (hostname, ip, online) und Funktionen (start, stop, status) logisch zusammengefasst sind. Das erleichtert spätere Erweiterungen, z.B. um weitere Funktionen wie restart oder update.

Warum nicht nur Dictionaries?

Alternativ könnten Sie Serverdaten auch in einem Dictionary speichern:

server = {"hostname": "web01", "ip": "192.168.1.10", "online": False}

Doch dann fehlen die zugehörigen Funktionen, und Sie müssten Statusänderungen manuell verwalten. Klassen bieten hier eine bessere Kapselung und verhindern Fehler durch falsche Zustandsänderungen.

Ein Schritt weiter: Objekte als praktische Werkzeuge

Klassen sind besonders nützlich, wenn Sie wiederholt mit ähnlichen Daten arbeiten und diese mit spezifischem Verhalten verknüpfen möchten. Zum Beispiel könnten Sie eine Klasse BackupJob definieren, die Pfade, Zeitpläne und Backup-Methoden zusammenfasst.

Best Practice: Klare, einfache Klassen

  • Halten Sie Klassen schlank: Ein Objekt sollte eine klar definierte Aufgabe haben.
  • Nutzen Sie sprechende Methodennamen, die genau beschreiben, was die Funktion macht.
  • Vermeiden Sie unnötige Vererbungen oder komplexe Hierarchien – das erleichtert das Verständnis.

Zusammenfassung

Klassen helfen Ihnen, zusammengehörige Daten und Funktionen in der Infrastrukturautomation übersichtlich zu organisieren. Sie sind ein Werkzeug, um komplexe Abläufe besser zu strukturieren und wiederverwendbaren, wartbaren Code zu schreiben. Im nächsten Abschnitt lernen Sie, wie Sie mit dataclasses noch schneller einfache Datenstrukturen definieren können.

Dataclasses

Warum Dataclasses?

Viele Klassen dienen hauptsächlich dazu, Daten zu speichern. Solche Klassen enthalten oft nur Attribute sowie einen Konstruktor zur Initialisierung dieser Attribute. Vor der Einführung von Dataclasses musste dafür vergleichsweise viel Boilerplate-Code geschrieben werden.

Ein klassisches Beispiel:

class Customer:
    def __init__(
        self,
        customer_id: int,
        name: str,
        email: str,
    ) -> None:
        self.customer_id = customer_id
        self.name = name
        self.email = email

Diese Klasse speichert lediglich Daten. Die eigentliche Logik beschränkt sich auf die Zuweisung der Parameter zu den Attributen.

Mit Dataclasses lässt sich derselbe Sachverhalt deutlich kompakter ausdrücken.

Erste Dataclass

Dataclasses wurden in Python 3.7 eingeführt und befinden sich im Standardmodul dataclasses.

from dataclasses import dataclass


@dataclass
class Customer:
    customer_id: int
    name: str
    email: str

Die Dekoratorfunktion @dataclass erzeugt automatisch:

  • einen Konstruktor (__init__)
  • eine lesbare Textdarstellung (__repr__)
  • Vergleichsoperatoren (__eq__)
  • weitere Hilfsmethoden

Die Klasse kann sofort verwendet werden:

customer = Customer(
    customer_id=1,
    name="Alice",
    email="alice@example.com",
)

print(customer)

Ausgabe:

Customer(
    customer_id=1,
    name='Alice',
    email='alice@example.com'
)

Was erzeugt eine Dataclass automatisch?

Die folgende Darstellung zeigt die wichtigsten automatisch generierten Bestandteile einer Dataclass:

classDiagram

    class Customer {
        +int customer_id
        +str name
        +str email
    }

    Customer : __init__()
    Customer : __repr__()
    Customer : __eq__()

Dadurch entfällt ein großer Teil des wiederkehrenden Codes.

Standardwerte definieren

Attribute können Standardwerte erhalten.

from dataclasses import dataclass


@dataclass
class ServerConfig:
    host: str = "localhost"
    port: int = 8000
    debug: bool = False

Verwendung:

config = ServerConfig()

print(config)

Ausgabe:

ServerConfig(
    host='localhost',
    port=8000,
    debug=False
)

Natürlich können einzelne Werte überschrieben werden:

config = ServerConfig(
    host="api.example.com",
    port=443,
)

Dataclasses mit Methoden

Dataclasses sind ganz normale Klassen. Sie können beliebige Methoden enthalten.

from dataclasses import dataclass


@dataclass
class Rectangle:
    width: float
    height: float

    def area(self) -> float:
        return self.width * self.height

Verwendung:

rectangle = Rectangle(
    width=5,
    height=3,
)

print(
    rectangle.area()
)

Ausgabe:

15

Dataclasses ersetzen also keine objektorientierte Programmierung, sondern reduzieren lediglich den Aufwand für die Definition von Datenobjekten.

Mutable Standardwerte vermeiden

Ein häufiger Fehler besteht darin, veränderbare Objekte als Standardwert zu verwenden.

Falsch:

from dataclasses import dataclass


@dataclass
class Team:
    members: list[str] = []

Python würde dieselbe Liste für alle Instanzen verwenden.

Stattdessen sollte field() verwendet werden:

from dataclasses import dataclass
from dataclasses import field


@dataclass
class Team:
    members: list[str] = field(
        default_factory=list
    )

Jetzt erhält jede Instanz ihre eigene Liste.

Nachträgliche Initialisierung mit post_init

Manchmal müssen nach der automatischen Initialisierung weitere Berechnungen erfolgen.

Hierfür existiert die Methode __post_init__().

from dataclasses import dataclass


@dataclass
class Employee:
    first_name: str
    last_name: str

    def __post_init__(self) -> None:
        self.full_name = (
            f"{self.first_name} "
            f"{self.last_name}"
        )

Verwendung:

employee = Employee(
    "Alice",
    "Miller",
)

print(
    employee.full_name
)

Ausgabe:

Alice Miller

Die Methode wird automatisch nach dem erzeugten Konstruktor ausgeführt.

Unveränderliche Dataclasses

Für Konfigurationsobjekte oder andere unveränderliche Datenstrukturen kann eine Dataclass eingefroren werden.

from dataclasses import dataclass


@dataclass(frozen=True)
class ApiConfig:
    host: str
    port: int

Verwendung:

config = ApiConfig(
    host="localhost",
    port=8000,
)

config.port = 9000

Dies führt zu einer Ausnahme:

FrozenInstanceError

Dadurch lassen sich versehentliche Änderungen verhindern.

Dataclasses in der Praxis

Dataclasses eignen sich besonders für:

  • Konfigurationsobjekte
  • DTOs (Data Transfer Objects)
  • API-Anfragen und Antworten
  • Daten aus CSV-Dateien
  • Daten aus Datenbanken
  • Nachrichten zwischen Komponenten
  • Machine-Learning-Datensätze

Beispiel eines API-Datenobjekts:

from dataclasses import dataclass


@dataclass
class User:
    id: int
    name: str
    email: str

Nach dem Laden eines JSON-Dokuments können die Daten bequem in einer typisierten Struktur gespeichert werden.

Dataclasses und Type Hints

Dataclasses profitieren besonders von Type Hints.

from dataclasses import dataclass


@dataclass
class Product:
    id: int
    name: str
    price: float

Werkzeuge wie Mypy oder moderne IDEs können dadurch Fehler frühzeitig erkennen und Autovervollständigung bereitstellen.

Dataclasses versus klassische Klassen

Für reine Datenobjekte bieten Dataclasses zahlreiche Vorteile:

Klassische Klasse Dataclass
Viel Boilerplate Wenig Boilerplate
Konstruktor manuell Konstruktor automatisch
repr manuell repr automatisch
Vergleich oft manuell Vergleich automatisch
Mehr Schreibaufwand Kompakte Syntax

Sobald eine Klasse hauptsächlich Daten speichert, ist eine Dataclass häufig die bessere Wahl.

Zusammenfassung

Dataclasses gehören zu den nützlichsten Erweiterungen moderner Python-Versionen. Sie reduzieren Boilerplate-Code erheblich und machen Datenobjekte kompakter, lesbarer und leichter wartbar. Durch die automatische Generierung von Konstruktoren, Vergleichsoperatoren und Textdarstellungen können sich Entwickler auf die eigentliche Fachlogik konzentrieren. Insbesondere in Anwendungen mit vielen Datenobjekten gehören Dataclasses heute zum Standardwerkzeug professioneller Python-Entwicklung.

Wann Klassen sinnvoll sind

Wann Klassen einen Mehrwert bieten

In der Systemadministration und Infrastrukturautomatisierung begegnen Sie häufig Aufgaben, bei denen Daten und Funktionen eng zusammengehören. Klassen sind dann sinnvoll, wenn Sie diese Zusammengehörigkeit klar und übersichtlich strukturieren möchten.

Beispiel: Sie verwalten Server mit verschiedenen Eigenschaften (Hostname, IP-Adresse, Betriebssystem) und möchten Funktionen implementieren, die auf diesen Daten operieren (z.B. Status prüfen, Logs sammeln). Statt lose Datenstrukturen und separate Funktionen zu verwenden, können Sie eine Klasse Server definieren, die Daten und Methoden bündelt:

class Server:
    def __init__(self, hostname, ip, os):
        self.hostname = hostname
        self.ip = ip
        self.os = os

    def ping(self):
        import subprocess
        result = subprocess.run(['ping', '-c', '1', self.ip], capture_output=True)
        return result.returncode == 0

    def __str__(self):
        return f"Server {self.hostname} ({self.ip}) running {self.os}"

# Beispielnutzung
srv = Server('web01', '192.168.1.10', 'Ubuntu 22.04')
print(srv)
if srv.ping():
    print('Server ist erreichbar')
else:
    print('Server ist nicht erreichbar')

Hier bietet die Klasse klare Vorteile: - Bündelung von Daten und Verhalten: IP, Hostname und Betriebssystem sind eng mit den Methoden verknüpft. - Wiederverwendbarkeit: Sie können beliebig viele Server-Objekte mit eigenen Eigenschaften erzeugen. - Übersichtlichkeit: Code und Daten sind logisch gruppiert, was Wartung erleichtert.

Wann einfache Datentypen oder Funktionen ausreichen

Für einfache, einmalige oder flache Aufgaben sind Klassen oft überdimensioniert. Wenn Sie beispielsweise nur eine Liste von IP-Adressen verwalten und eine einfache Funktion zum Prüfen der Erreichbarkeit brauchen, sind Listen und Funktionen oft schneller und klarer:

def ping(ip):
    import subprocess
    result = subprocess.run(['ping', '-c', '1', ip], capture_output=True)
    return result.returncode == 0

ips = ['192.168.1.10', '192.168.1.11']
for ip in ips:
    status = 'erreichbar' if ping(ip) else 'nicht erreichbar'
    print(f'{ip} ist {status}')

Hier ist kein Overhead durch Klassen nötig, da nur einfache Daten und eine einzelne Funktion verwendet werden.

Wann Dictionaries oder Dataclasses sinnvoll sind

Wenn Sie strukturierte Daten ohne komplexes Verhalten speichern wollen, sind Dictionaries oder dataclasses oft die beste Wahl. Sie sind leichter und flexibler als Klassen mit vielen Methoden.

from dataclasses import dataclass

@dataclass
class BackupJob:
    source: str
    destination: str
    schedule: str

job = BackupJob('/var/log', '/backup/logs', 'daily')
print(job)

Dataclasses bieten eine einfache Möglichkeit, strukturierte Konfigurationsdaten oder Statusinformationen zu modellieren, ohne viel Boilerplate-Code.

Zusammenfassung

Situation Empfehlung
Daten und zugehörige Funktionen bündeln Eigene Klassen verwenden
Einfache, flache Daten und einzelne Funktionen Listen, Dictionaries und Funktionen
Strukturierte Daten ohne komplexes Verhalten Dataclasses nutzen

Nutzen Sie Klassen, wenn Ihre Automatisierung wächst und Sie klare, wiederverwendbare Bausteine benötigen. Für kleine, einmalige Aufgaben bleiben einfache Funktionen und Datenstrukturen oft der schnellere Weg.