1 Objektorientierte Programmierung

1.1 Einführung

Modularität bezieht sich auf die Fähigkeit, ein großes Programm in kleinere, unabhängige Module zu unterteilen. In der OOP werden diese Module als Klassen bezeichnet. Jede Klasse kapselt einen bestimmten Funktionsbereich und ermöglicht die Definition von Schnittstellen, die die Interaktion zwischen den Modulen erleichtern. Durch diese klare Trennung von Verantwortlichkeiten ist es einfacher, den Code zu verstehen, zu entwickeln und zu testen.

Wiederverwendbarkeit ist ein weiterer Vorteil der OOP. Da die Klassen bestimmte Funktionalitäten und Verhaltensweisen kapseln, können sie in verschiedenen Projekten oder Programmteilen wiederverwendet werden, ohne dass der Code jedes Mal neu geschrieben werden muss. Dies führt zu einer effizienteren Entwicklung und ermöglicht es, auf bewährte Lösungen zurückzugreifen.

Einfache Wartung ist ein zusätzlicher Nutzen der objektorientierten Programmierung. Durch die Modularität und Wiederverwendbarkeit des Codes wird die Wartung und Fehlerbehebung erleichtert. Änderungen an einer Klasse haben in der Regel keine weitreichenden Auswirkungen auf andere Teile des Programms, da die Kapselung sicherstellt, dass Änderungen lokal begrenzt sind. Dies ermöglicht es, bestehenden Code leichter zu aktualisieren, zu erweitern oder zu optimieren.

Insgesamt bieten die Vorteile der OOP – Modularität, Wiederverwendbarkeit und einfache Wartung – einen effektiven Ansatz zur Entwicklung von Software, der dazu beiträgt, die Komplexität zu bewältigen und die Produktivität der Entwickler zu steigern.

1.2 Klassen und Objekte vertiefen

class MyClass:
    class_attribute = "Shared by all instances"

    def __init__(self, instance_attribute):
        self.instance_attribute = instance_attribute

    def __del__(self):
        print(f"Instance {self} is being destroyed")

    def instance_method(self):
        print(f"Instance method called on {self.instance_attribute}")

    @classmethod
    def class_method(cls):
        print(f"Class method called on {cls.class_attribute}")

# Create instances of MyClass
obj1 = MyClass("Object 1")
obj2 = MyClass("Object 2")

# Access class attribute and class method
print(MyClass.class_attribute)
MyClass.class_method()

# Access instance attributes and instance methods
print(obj1.instance_attribute)
obj1.instance_method()

print(obj2.instance_attribute)
obj2.instance_method()

1.3 Vererbung

class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        print(f"{self.name} makes a noise")

class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)
        self.breed = breed

    def speak(self):
        super().speak()
        print(f"{self.name} the {self.breed} says Woof!")

class Cat(Animal):
    def speak(self):
        print(f"{self.name} says Meow!")

class Hybrid(Dog, Cat):
    def __init__(self, name, breed):
        super().__init__(name, breed)

    def speak(self):
        print(f"{self.name} the {self.breed} hybrid makes a unique noise")

# Create instances of Animal, Dog, Cat, and Hybrid
animal = Animal("Generic Animal")
dog = Dog("Buddy", "Golden Retriever")
cat = Cat("Whiskers")
hybrid = Hybrid("Fido", "Mysterious Mix")

# Call the speak() method on each instance
animal.speak()
dog.speak()
cat.speak()
hybrid.speak()

1.4 Abstrakte Klassen und Methoden

from abc import ABC, abstractmethod

class Shape(ABC):

    @abstractmethod
    def area(self):
        pass

    @abstractmethod
    def perimeter(self):
        pass

class Rectangle(Shape):

    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

    def perimeter(self):
        return 2 * (self.width + self.height)

class Circle(Shape):

    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.14159 * self.radius ** 2

    def perimeter(self):
        return 2 * 3.14159 * self.radius

# Create instances of Rectangle and Circle
rect = Rectangle(10, 5)
circle = Circle(7)

# Call area() and perimeter() methods on each instance
print(rect.area(), rect.perimeter())
print(circle.area(), circle.perimeter())

Das abc-Modul in Python steht für “Abstract Base Class” und bietet Mechanismen zur Definition von abstrakten Klassen und Methoden. Abstrakte Klassen sind Klassen, die nicht direkt instanziiert werden können. Sie dienen als Vorlagen oder Basisklassen für konkrete Klassen, die ihre Methoden implementieren müssen.

Das abc-Modul hilft dabei, eine klare Schnittstelle für Klassen in einer Klassenhierarchie zu definieren, indem es erzwingt, dass abstrakte Methoden in den Subklassen implementiert werden. Wenn eine abstrakte Methode in einer Subklasse nicht implementiert wird, führt dies zu einem Fehler bei dem Versuch, ein Objekt dieser Subklasse zu erstellen.

In einem abc-Modul gibt es zwei Hauptkomponenten:

  1. ABC: Es ist eine Metaklasse, die dazu verwendet wird, eine Klasse als abstrakte Klasse zu kennzeichnen. Um eine Klasse als abstrakte Klasse zu definieren, muss sie von ABC erben, wie zum Beispiel class MyAbstractClass(ABC):.

  2. abstractmethod: Es ist ein Dekorator, der auf Methoden in einer abstrakten Klasse angewendet wird, um sie als abstrakte Methoden zu kennzeichnen. Eine abstrakte Methode muss in jeder konkreten (nicht-abstrakten) Subklasse implementiert werden. Der Dekorator wird in Kombination mit der Methodendefinition verwendet, z. B. @abstractmethod def my_abstract_method(self):.

Das abc-Modul fördert somit eine gute Programmierpraxis, indem es klar definierte Schnittstellen für Klassen vorschreibt und sicherstellt, dass diese Schnittstellen in den abgeleiteten Klassen ordnungsgemäß implementiert werden.

1.5 Polymorphismus

class Dog:
    def __init__(self, name):
        self.name = name

    def speak(self):
        return f"{self.name} says Woof!"

class Cat:
    def __init__(self, name):
        self.name = name

    def speak(self):
        return f"{self.name} says Meow!"

def animal_speak(animal):
    print(animal.speak())

# Create instances of Dog and Cat
dog = Dog("Buddy")
cat = Cat("Whiskers")

# Demonstrate polymorphic behavior
animal_speak(dog)
animal_speak(cat)

# Demonstrate duck-typing in Python
animals = [dog, cat]
for animal in animals:
    print(animal.speak())

In diesem Beispiel haben wir zwei Klassen, Dog und Cat, die jeweils eine speak-Methode haben. Obwohl die Klassen nicht von derselben Basisklasse erben, haben sie eine ähnliche Schnittstelle und können polymorph verwendet werden. Die Funktion animal_speak akzeptiert ein Objekt der Klasse Dog oder Cat und ruft deren speak-Methode auf, um das entsprechende Tiergeräusch auszugeben.

Duck-Typing in Python ist ein Konzept, bei dem die Typüberprüfung von Objekten nicht anhand ihrer Klassenzugehörigkeit, sondern anhand ihrer verfügbaren Methoden oder Attribute durchgeführt wird. Im obigen Beispiel wird eine Liste von Dog- und Cat-Objekten erstellt und in einer Schleife durchlaufen, um deren speak-Methode aufzurufen, ohne dass der Code prüft, ob das Objekt ein Hund oder eine Katze ist. Dies ist ein Beispiel für Duck-Typing, da es das Verhalten des Objekts betrachtet, anstatt auf seinen Typ zu achten.

1.6 Kapselung

class Employee:
    def __init__(self, name, salary):
        self.name = name
        self._salary = salary  # _salary is protected (by convention)
        self.__bonus = 0       # __bonus is private

    # Getter method for salary
    def get_salary(self):
        return self._salary

    # Setter method for salary
    def set_salary(self, new_salary):
        if new_salary >= 0:
            self._salary = new_salary
        else:
            print("Invalid salary")

    # Getter method for bonus (private attribute)
    def get_bonus(self):
        return self.__bonus

    # Setter method for bonus (private attribute)
    def set_bonus(self, new_bonus):
        if new_bonus >= 0:
            self.__bonus = new_bonus
        else:
            print("Invalid bonus")

    # Property decorators for salary
    salary = property(get_salary, set_salary)

    # Property decorators for bonus
    bonus = property(get_bonus, set_bonus)

# Create an Employee instance
employee = Employee("John Doe", 50000)

# Accessing public attributes and properties
print(employee.name)
print(employee.salary)

# Modifying attributes and properties
employee.salary = 55000
print(employee.salary)

# Accessing and modifying protected attributes directly is possible, but not recommended
employee._salary = 60000
print(employee._salary)

# Accessing and modifying private attributes is not possible directly
# print(employee.__bonus)  # This line would raise an AttributeError
employee.bonus = 5000
print(employee.bonus)

In diesem Beispiel haben wir eine Employee-Klasse erstellt, die Kapselung demonstriert. Die Klasse hat öffentliche, geschützte und private Attribute (name, _salary, __bonus). Durch Konvention werden geschützte Attribute durch einen einzelnen Unterstrich gekennzeichnet, während private Attribute durch zwei Unterstriche gekennzeichnet werden.

Getter- und Setter-Methoden werden verwendet, um den Zugriff auf und die Änderung von geschützten und privaten Attributen zu ermöglichen. Die property-Dekoratoren erstellen Eigenschaften für die geschützten und privaten Attribute, um direkten Zugriff und Änderung zu verhindern und stattdessen die Getter- und Setter-Methoden zu verwenden.

Im Beispiel haben wir gezeigt, wie man auf öffentliche Attribute und Eigenschaften zugreift und sie ändert. Der direkte Zugriff auf geschützte Attribute ist möglich, wird jedoch nicht empfohlen. Private Attribute können nicht direkt zugegriffen oder geändert werden, weshalb wir stattdessen die Getter- und Setter-Methoden verwenden.