Klassen und Objekte#

Ein fundamentales Konzept in der objektorientierten Programmierung (OOP) sind Klassen und Objekte.

  • Eine Klasse ist eine Art Bauplan oder Vorlage, die beschreibt, wie ein Objekt aussehen soll und welche Eigenschaften (Attribute) und Fähigkeiten (Methoden) es haben soll.

  • Ein Objekt ist eine konkrete Instanz einer Klasse, die tatsächlich im Speicher existiert und verwendet werden kann.

Warum Klassen?#

  • Klassen helfen dabei, den Code zu strukturieren und zu organisieren, indem sie verwandte Daten und Funktionen zusammenfassen.

  • Sie ermöglichen die Wiederverwendung von Code, da einmal definierte Klassen mehrfach instanziiert werden können.

  • Klassen unterstützen Konzepte wie Vererbung (später mehr dazu), wodurch neue Klassen auf bestehenden Klassen basieren können.

Eine Klasse ist ein Datentyp, und ein Objekt ist eine Variable dieses Datentyps.

Definition einer Klasse#

Die Syntax zur Definition einer Klasse in Python ist wie folgt:

class ClassName:
    ### Beschreibung der Klasse
  • class ist ein Schlüsselwort in Python, das die Definition einer Klasse einleitet.

  • ClassName ist der Name der Klasse und jedes Wort sollte mit einem Großbuchstaben beginnen (CamelCase oder CapWords Konvention).

  • Achtung: Einrückung und Doppelpunkt : nicht vergessen

Die Beschreibung der Klasse beginnt meistens mit dem Konstruktor

  • Der Konstruktor definiert was passiert, wenn ein Objekt der Klasse erstellt wird (= eine Variable des Typs angelegt wird)

    def __init__(self): # ACHTUNG: Doppelter Unterstrich vor und nach init
        ### Initialisierung der Attribute

self ist der Name des aktuellen Objekts der Klasse und wird automatisch an den Konstruktor übergeben. Im folgenden Beispiel werden drei Attribute im Konstruktor definiert:

class EPROG:
    def __init__(self):
        self.name = "Einführung in die Programmierung"
        self.lecturer = "Michael Feischl"
        self.topics = ["Python", "Variablen", "Kontrollstrukturen", "Funktionen", "Klassen und Objekte"]

Um Objekte einer Klasse zu erstellen, verwendet man die Syntax:

obj = ClassName()

Mit dem Punktoperator (.) greift man auf die Attribute eines Objekts zu:

lecture = EPROG()
print(f"Modul: {lecture.name}, Dozent: {lecture.lecturer}, Themen: {lecture.topics}")
Modul: Einführung in die Programmierung, Dozent: Michael Feischl, Themen: ['Python', 'Variablen', 'Kontrollstrukturen', 'Funktionen', 'Klassen und Objekte']

Man kann die Werte der Attribute im Nachhinein ändern:

lecture1 = EPROG()
lecture2 = EPROG()
print(f"Modul: {lecture1.name}, Dozent: {lecture1.lecturer}, Themen: {lecture1.topics}")
print(f"Modul: {lecture2.name}, Dozent: {lecture2.lecturer}, Themen: {lecture2.topics}")
lecture1.lecturer = "Rando. M. Dude"
lecture1.name = "Die Kartoffel als Gemüse und Wurfgeschoss"
lecture1.topics.append("Kartoffeln")
print(f"Modul: {lecture1.name}, Dozent: {lecture1.lecturer}, Themen: {lecture1.topics}")
print(f"Modul: {lecture2.name}, Dozent: {lecture2.lecturer}, Themen: {lecture2.topics}")
Modul: Einführung in die Programmierung, Dozent: Michael Feischl, Themen: ['Python', 'Variablen', 'Kontrollstrukturen', 'Funktionen', 'Klassen und Objekte']
Modul: Einführung in die Programmierung, Dozent: Michael Feischl, Themen: ['Python', 'Variablen', 'Kontrollstrukturen', 'Funktionen', 'Klassen und Objekte']
Modul: Die Kartoffel als Gemüse und Wurfgeschoss, Dozent: Rando. M. Dude, Themen: ['Python', 'Variablen', 'Kontrollstrukturen', 'Funktionen', 'Klassen und Objekte', 'Kartoffeln']
Modul: Einführung in die Programmierung, Dozent: Michael Feischl, Themen: ['Python', 'Variablen', 'Kontrollstrukturen', 'Funktionen', 'Klassen und Objekte']

Man kann auch neue Attribute zu einem Objekt hinzufügen

  • Eher unüblich und sollte vermieden werden

  • Konvention: Alle Attribute sollten im Konstruktor definiert werden

lecture3 = EPROG()
lecture3.ects = 6  # Neues Attribut wird hinzugefügt
print(f"Modul: {lecture3.name}, Dozent: {lecture3.lecturer}, Themen: {lecture3.topics}, ECTS: {lecture3.ects}")
Modul: Einführung in die Programmierung, Dozent: Michael Feischl, Themen: ['Python', 'Variablen', 'Kontrollstrukturen', 'Funktionen', 'Klassen und Objekte'], ECTS: 6

Beispiel zu Konstruktoren#

Der Konstruktor wird automatisch aufgerufen, wenn ein Objekt der Klasse erstellt wird.

class Test:
    def __init__(self):
        print("Ein Objekt der Klasse Test wurde erstellt.")

t1 = Test()
t2 = Test()
Ein Objekt der Klasse Test wurde erstellt.
Ein Objekt der Klasse Test wurde erstellt.

Methoden der Klasse#

Methoden sind Funktionen, die innerhalb einer Klasse definiert sind und auf die Attribute der Klasse zugreifen können. Sie werden verwendet, um das Verhalten von Objekten zu definieren. Da Methoden auf die Attribute des Objekts zugreifen können, haben sie immer self als ersten Parameter. Ansonsten gelten die gleichen Regeln wie bei normalen Funktionen.

    def method_name(self, parameters):
        ### Methode, die etwas mit den Attributen macht
class EPROG:
    def __init__(self):
        self.name = "Einführung in die Programmierung"
        self.lecturer = "Michael Feischl"
        self.topics = ["Python", "Variablen", "Kontrollstrukturen", "Funktionen", "Klassen und Objekte"]
        self.student_list = []

    def add_student(self, student_name):
        self.student_list.append(student_name)
    def list_students(self):
        print("Eingeschriebene Studierende:")
        for student in self.student_list:
            print(f"- {student}")
        
lecture = EPROG()
lecture.add_student("Alice")
lecture.add_student("Bob")
lecture.list_students()
Eingeschriebene Studierende:
- Alice
- Bob

Beispiel: Konstruktor mit Parametern#

Der Konstruktor kann auch weitere Parameter haben, um die Attribute eines Objekts bei der Erstellung zu initialisieren.

class Test:
    def __init__(self, param1, param2):        
        print(f"Ein Objekt der Klasse Test mit Parametern {param1} und {param2} wurde erstellt.")

t1 = Test("Wert1", "Wert2")
t2 = Test("Wert3", "Wert4")
Ein Objekt der Klasse Test mit Parametern Wert1 und Wert2 wurde erstellt.
Ein Objekt der Klasse Test mit Parametern Wert3 und Wert4 wurde erstellt.

Ein nützlicheres Beispiel ist eine Klasse Triangle, die die Eigenschaften eines Dreiecks beschreibt:

class Triangle:
    def __init__(self, a, b, c):
        # Store nodes of triangle as tuples or list (x,y)
        self.a = a
        self.b = b
        self.c = c
        self.ab = ((b[0]-a[0])**2 + (b[1]-a[1])**2)**0.5
        self.bc = ((c[0]-b[0])**2 + (c[1]-b[1])**2)**0.5
        self.ca = ((a[0]-c[0])**2 + (a[1]-c[1])**2)**0.5

    def perimeter(self):
        return self.ab + self.bc + self.ca

    def area(self):
        # use Heron's formula Area = sqrt(s*(s-a)*(s-b)*(s-c)) with s = perimeter/2
        s = self.perimeter()/2
        return (s*(s-self.ab)*(s-self.bc)*(s-self.ca))**0.5
    
triangle = Triangle((0,0), (3,0), (0,4))
print(f"Umfang: {triangle.perimeter()}, Fläche: {triangle.area()}")
Umfang: 12.0, Fläche: 6.0

Ein weiters Beispiel ist die Klasse Polygon, die eine allgemeine Form mit einer bestimmten Anzahl von Eckpunkten beschreibt

class Polygon:
    def __init__(self, vertices):
        self.vertices = vertices
        self.edges = []
        for i in range(len(vertices)):
            j = (i + 1) % len(vertices)  # next vertex, wrapping around
            edge_length = ((vertices[j][0] - vertices[i][0])**2 + (vertices[j][1] - vertices[i][1])**2)**0.5
            self.edges.append(edge_length)

    def perimeter(self):
        return sum(self.edges)
    
    def number_of_vertices(self):
        return len(self.vertices)
    
quad = Polygon([(0,0), (4,0), (4,3), (0,3)])
print(f"Anzahl der Ecken: {quad.number_of_vertices()}, Umfang: {quad.perimeter()}")
Anzahl der Ecken: 4, Umfang: 14.0

Statische Methoden und Attribute#

Manchmal (selten) macht es Sinn Attribute oder Methoden zu definieren, die nicht an eine bestimmte Instanz der Klasse gebunden sind, sondern zur Klasse selbst gehören. Diese werden als statische Attribute und statische Methoden bezeichnet.

  • Statische Attribute werden außerhalb des Konstruktors definiert

    • Statische Attribute sollten nicht gändert werden, da sich Änderungen auf alles Objekte der Klasse auswirken

  • Statische Methoden werden mit dem Dekorator @staticmethod definiert und haben keinen self Parameter

class ClassName:
    static_attribute = value  # Statisches Attribut

    @staticmethod
    def static_method():
        # Methode, die nicht an eine Instanz gebunden ist
  • Statische Methoden können nützlich sein, wenn mehrere Funktionalitäten zusammengefasst werden sollen, aber kein Objekt benötigt wird.

  • Statische Methoden können nicht auf nicht-statische Attribute der Klasse zugreifen, da sie keinen self Parameter haben.

  • Statische Attribute folgen nicht den Scope-Regeln von normalen Variablen, sondern müssen immer mit dem Klassennamen referenziert werden.

    • Beispiel: Unten wird Circle.pi verwendet, um auf das statische Attribut pi zuzugreifen.

class Circle:
    pi = 3.14159  # static attribute

    @staticmethod
    def circle_area(radius):
        return Circle.pi * radius * radius

    @staticmethod
    def circle_perimeter(radius):
        return 2 * Circle.pi * radius

    def __init__(self, radius):
        self.radius = radius  # non-static attribute        

    def area(self):
        return Circle.pi * self.radius**2

    def perimeter(self):
        return 2 * Circle.pi * self.radius

circle1 = Circle(5)
print(f"Fläche des Kreises mit Radius {circle1.radius}: {circle1.area()}")
print(f"Umfang des Kreises mit Radius {circle1.radius}: {circle1.perimeter()}")

print(f"Fläche des Kreises mit Radius 7 (statisch): {Circle.circle_area(7)}")
print(f"Umfang des Kreises mit Radius 7 (statisch): {Circle.circle_perimeter(7)}")
Fläche des Kreises mit Radius 5: 78.53975
Umfang des Kreises mit Radius 5: 31.4159
Fläche des Kreises mit Radius 7 (statisch): 153.93791
Umfang des Kreises mit Radius 7 (statisch): 43.98226

Vergisst man den Dekorator @staticmethod, wird die Methode als normale Methode interpretiert und erwartet einen self Parameter. Ein Aufruf mit Objekt führt dann zu einem Fehler.

class Circle:
    pi = 3.14159  # static attribute

    def circle_area(radius):
        return Circle.pi * radius * radius
    
print(f"Fläche eines Kreises mit Radius 5: {Circle.circle_area(5)}")

circ1 = Circle()
print(f"Fläche eines Kreises mit Radius 7: {circ1.circle_area(7)}")
Fläche eines Kreises mit Radius 5: 78.53975
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[12], line 10
      7 print(f"Fläche eines Kreises mit Radius 5: {Circle.circle_area(5)}")
      9 circ1 = Circle()
---> 10 print(f"Fläche eines Kreises mit Radius 7: {circ1.circle_area(7)}")

TypeError: Circle.circle_area() takes 1 positional argument but 2 were given

Beispiel: Instanzen einer Klasse zählen#

Statische Attribute können verwendet werden, um z.B. die Anzahl der erstellten Instanzen einer Klasse zu zählen

class Student:
    number_of_students = 0  # static attribute

    def __init__(self, name):
        self.name = name
        self.grade = None
        Student.number_of_students += 1  # increment static attribute

    @staticmethod
    def get_number_of_students():
        return Student.number_of_students

    def compute_grade(self, points):
        if points >= 90:
            self.grade = 'A'
        elif points >= 80:
            self.grade = 'B'
        elif points >= 70:
            self.grade = 'C'
        elif points >= 60:
            self.grade = 'D'
        else:
            self.grade = 'F'

s1 = Student("Alice")
s2 = Student("Bob")
s1.compute_grade(85)
s2.compute_grade(92)
print(f"{s1.name} hat Note {s1.grade}")
print(f"{s2.name} hat Note {s2.grade}")
print(f"Anzahl der Studierenden: {Student.get_number_of_students()}")
Alice hat Note B
Bob hat Note A
Anzahl der Studierenden: 2

Namensgleichheit von statischen und nicht-statischen Attributen#

Ein Zugriff auf self.a oder obj.a sucht zuerst nach einem nicht-statischen Attribut a der Instanz. Wenn dieses nicht existiert, wird nach einem statischen Attribut a der Klasse gesucht.

class Test:
    a = 10  # Statisches Attribut der Klasse
    b = 15  # Statisches Attribut der Klasse
    def __init__(self):
        self.a = 20  # Instanzattribut der Instanz

obj = Test()
print(f"obj.a = {obj.a}")  # Greift auf das Instanzattribut zu, das statische Attribut wird verdeckt
print(f"obj.b = {obj.b}")  # Greift auf das statische Attribut zu
print(f"Test.a = {Test.a}")  # Greift auf das statische Attribut der Klasse zu
obj.a = 20
obj.b = 15
Test.a = 10

(Fast) Alles ist ein Objekt#

In Python ist alles ein Objekt, z.B. auch Funktionen und Klassen selbst.

  • Man kann neue Attribute zu Funktionen hinzufügen

    • Erschwert die Lesbarkeit des Codes und sollte vermieden werden

  • Funktionen haben vordefinierte Attribute, wie __name__, __doc__, __closure__, __dict__, __code__, __annotations__, __defaults__ und __module__.

  • Funktionen haben auch ein self Attribut, wenn sie als Methoden in einer Klasse definiert sind.

  • Funktion haben auch Methoden, wie __call__(), __get__(),…

    • Diese Methoden werden selten direkt verwendet, sondern sind für spezielle Anwendungsfälle gedacht.

def test_function(x):
    return x * x

test_function.var = 5
print("test_function.var =", test_function.var)

print("test_function.__name__ =", test_function.__name__)
print("test_function.__doc__ =", test_function.__doc__)
print("test_function.__module__ =", test_function.__module__)
test_function.var = 5
test_function.__name__ = test_function
test_function.__doc__ = None
test_function.__module__ = __main__

Mit einfachen Variablen funktioniert das nicht. Diese sind in Python optimiert und können daher keine neuen Attribute bekommen.

x = [1,2,4]
x.var = 5
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[22], line 2
      1 x = [1,2,4]
----> 2 x.var = 5

AttributeError: 'list' object has no attribute 'var'

Zugriffskontrolle#

Waurm ist Zugriffskontrolle wichtig?

  • In vielen Programmiersprachen gibt es Mechanismen, um den Zugriff auf bestimmte Attribute oder Methoden einer Klasse zu beschränken.

  • Dies hilft, die Integrität der Daten zu schützen und verhindert, dass externe Code Teile die internen Details einer Klasse unbeabsichtigt verändern.

Im obigen Beispiel der Student Klasse könnte man beispielsweise einfach das Attribut number_of_students von außerhalb der Klasse ändern, ohne weitere Studierende anzulegen.

s3 = Student("Charlie")
print(f"Anzahl der Studierenden: {Student.get_number_of_students()}")
Student.number_of_students = 10  # Manipulation von außen
print(f"Anzahl der Studierenden: {Student.get_number_of_students()}")  
Anzahl der Studierenden: 3
Anzahl der Studierenden: 10

Wichtig: Zugriffskontrolle kann keine böswilligen Angriffe verhindern, sondern soll nur unabsichtliche Änderungen verhindern.

  • Dient der Fehlervermeidung: Was nicht verändert werden soll, kann auch nicht verändert werden

  • Führt zu besser wartbarem Code (vor allem in großen Projekten)

Zugriffskontrolle in Python#

In Python gibt es leider keine strikte Zugriffskontrolle wie in einigen anderen Programmiersprachen (z.B. private, protected, public in Java oder C++).

  • Alle Attribute und Methoden einer Klasse sind standardmäßig öffentlich (public) und können von außerhalb der Klasse zugegriffen werden.

  • Es gibt jedoch eine Konvention, um anzuzeigen, dass bestimmte Attribute oder Methoden als “privat” betrachtet werden sollten:

    • Ein einzelner Unterstrich (_) signalisiert, dass das Attribut oder die Methode als “geschützt” betrachtet werden sollte und nicht direkt von außerhalb der Klasse verwendet werden sollte.

    • Ein doppelter Unterstrich (__) führt zu einer internen Namensänderung (Name Mangling), die den direkten Zugriff von außerhalb der Klasse erschwert.

    • Achtung, mehr als ein Unterstrich am Ende des Names hebt den Schutz wieder auf (siehe z.bei __init__)

class Example:
    public_attr = "Ich bin öffentlich"
    _protected_attr = "Ich bin geschützt"
    __private_attr = "Ich bin privat"
    def public_method(self):
        print("Dies ist eine öffentliche Methode.")

    def _protected_method(self):
        print("Dies ist eine geschützte Methode.")

    def __private_method(self):
        print("Dies ist eine private Methode.")

ex = Example()
ex.public_method()          # Funktioniert
ex._protected_method()      # Funktioniert, aber sollte nicht verwendet werden
ex.__private_method()     # Führt zu einem Fehler
Dies ist eine öffentliche Methode.
Dies ist eine geschützte Methode.
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[37], line 17
     15 ex.public_method()          # Funktioniert
     16 ex._protected_method()      # Funktioniert, aber sollte nicht verwendet werden
---> 17 ex.__private_method()     # Führt zu einem Fehler

AttributeError: 'Example' object has no attribute '__private_method'

Tatsächlich ist der Zugriff auf solche “privaten” Methoden immer noch möglich. Der doppelte Unterstrich führt lediglich zu folgener internen Umbenennung:

  • Eine Methode __private_method in der Klasse MyClass wird intern zu _MyClass__private_method

ex._Example__private_method()  # Funktioniert, aber sollte nicht verwendet werden
print(ex._Example__private_attr)  # Funktioniert, aber sollte nicht verwendet werden
Dies ist eine private Methode.
Ich bin privat

Beispiel: Student Klasse mit Zugriffskontrolle#

class Student:
    __number_of_students = 0  # static attribute

    def __init__(self, name):
        self.name = name
        self.grade = None
        Student.__number_of_students += 1  # increment static attribute

    @staticmethod
    def get_number_of_students():
        return Student.__number_of_students

    def compute_grade(self, points):
        if points >= 90:
            self.grade = 'A'
        elif points >= 80:
            self.grade = 'B'
        elif points >= 70:
            self.grade = 'C'
        elif points >= 60:
            self.grade = 'D'
        else:
            self.grade = 'F'

s1 = Student("Alice")
s2 = Student("Bob")
print(f"Anzahl der Studierenden: {Student.get_number_of_students()}")
Anzahl der Studierenden: 2

Beispiel: Klasse für Brüche#

  • Verwende assert um Division durch Null zu verhindern

  • Kürze Brüche automatisch

class Fraction:
    def __init__(self, numerator, denominator):
        # Make sure that denominator is non-zero and both are integers
        assert denominator != 0, "Denominator cannot be zero"
        assert isinstance(numerator, int) and isinstance(denominator, int), "Numerator and Denominator must be integers"
        self.numerator = numerator
        self.denominator = denominator
        self.simplify()

    # Euclidean algorithm for GCD, used in simplify but can also be used independently of the Object
    @staticmethod
    def gcd(a, b):
            while b:
                a, b = b, a % b
            return a
    
    # Remove common factors from numerator and denominator and make sure that denominator is positive
    def simplify(self):
        common_divisor = Fraction.gcd(abs(self.numerator), abs(self.denominator))
        self.numerator //= common_divisor
        self.denominator //= common_divisor
        if self.denominator < 0:  # keep denominator positive
            self.numerator = -self.numerator
            self.denominator = -self.denominator

    def print(self):
        self.simplify()  # ensure fraction is simplified before printing
        if self.denominator == 1:
            print(f"{self.numerator}") # print only numerator if denominator is 1
        else:
            print(f"{self.numerator}/{self.denominator}")


# Use of GCD without creating an object
print(f"GCD of 48 and 18 is {Fraction.gcd(48, 18)}")

# Testing the Fraction class
f1 = Fraction(4, -8)
f1.print()  
f1.numerator = 10 
f1.print()
f2 = Fraction(10, 25)
f2.print() 
f3 = Fraction(3, 0)  # Löst AssertionError aus
GCD of 48 and 18 is 6
-1/2
5
2/5
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Cell In[1], line 44
     42 f2 = Fraction(10, 25)
     43 f2.print() 
---> 44 f3 = Fraction(3, 0)  # Löst AssertionError aus

Cell In[1], line 4, in Fraction.__init__(self, numerator, denominator)
      2 def __init__(self, numerator, denominator):
      3     # Make sure that denominator is non-zero and both are integers
----> 4     assert denominator != 0, "Denominator cannot be zero"
      5     assert isinstance(numerator, int) and isinstance(denominator, int), "Numerator and Denominator must be integers"
      6     self.numerator = numerator

AssertionError: Denominator cannot be zero

Beispiel: Klasse für die formatierte Ausgabe von Matrizen#

Die folgende Klasse soll Matrizen schön formatiert ausgeben. Dazu kann die Anzahl der gewünschten Nachkommastellen angegeben werden.

  • Die Klasse hat ein Attribut matrix, das die Matrix als Liste von Listen speichert.

  • Die Klasse hat eine Methode print, welche die Matrix formatiert ausgibt.

class MatrixPrinter:
    def __init__(self, matrix):
        self.matrix = matrix

    def print(self, precision=2):
        # Find maximum length of matrix entries for formatting
        max_len = max([len(f"{val:.{precision}f}") for row in self.matrix for val in row])
        # Print each row with formatted entries
        for row in self.matrix:
            print("|", " ".join(f"{val:{max_len}.{precision}f}" for val in row), "|")

matrix = MatrixPrinter([[1, 12.3456, 3], [4, 5, 6.789], [7.1, 8, 9]])
print("Matrix mit Standardgenauigkeit (2 Nachkommastellen):")
matrix.print()
print("Matrix mit 0 Nachkommastellen:")
matrix.print(0)
Matrix mit Standardgenauigkeit (2 Nachkommastellen):
|  1.00 12.35  3.00 |
|  4.00  5.00  6.79 |
|  7.10  8.00  9.00 |
Matrix mit 0 Nachkommastellen:
|  1 12  3 |
|  4  5  7 |
|  7  8  9 |

Die Code Zeile, welche die maximale Länge eines Eintrags in der Matrix bestimmt verdient eine genauere Erklärung:

max_len = max([len(f"{val:.{precision}f}") for row in self.matrix for val in row])
  • f"{val:.{precision}f}" ist ein f-String, der den Wert val als Fließkommazahl mit der angegebenen Anzahl von Nachkommastellen (precision) formatiert.

    • Hierbei werden die geschweiften Klammen geschachtelt, um die Variable precision innerhalb des f-Strings zu verwenden.

  • Die Methode len bestimmt dann die Länge des formatierten Strings, also die Anzahl der Zeichen die benötigt werden, um die Zahl darzustellen.

  • [... for row in self.matrix for val in row] erstellt eine Liste aller Längen der formatierten Strings, indem über alle Zeilen und Werte in der Matrix iteriert wird.

  • Die Funktion max bestimmt dann die maximale Länge aus dieser Liste, also die Länge des längsten Eintrags in der Matrix.

Die Ausgabe der Matrix erfolgt dann zeilenweise, wobei jeder Eintrag rechtsbündig mit der maximalen Länge formatiert wird. Dabei wird wieder ein f-String verwendet, wobei die Breite des Feldes durch max_len bestimmt wird

f"{val:{max_len}.{precision}f}"
  • Die Methode join verbindet die formatierten Einträge einer Zeile mit einem Leerzeichen zu einem einzigen String, der dann ausgegeben wird.

Magic Methods#

Magische Methoden (auch als “Dunder Methods” bezeichnet, da sie mit doppeltem Unterstrich beginnen und enden) sind spezielle Methoden in Python, die es ermöglichen, das Verhalten von Objekten zu definieren und anzupassen.

  • Magische Methoden beginnen und enden mit doppeltem Unterstrich

    • Beispiel: __init__

  • Magische Methoden werden automatisch von Python aufgerufen, wenn bestimmte Operationen auf Objekten ausgeführt werden

  • Sie sind nützlich um benutzerdefinierte Klassen nahtlos in Python zu integrieren und das Verhalten von Objekten zu steuern.

  • Eine Liste der wichtigsten magischen Methoden ist:

Magische Methode

Beschreibung

__init__(self, ...)

Konstruktor der Klasse, wird aufgerufen, wenn ein neues Objekt erstellt wird

__str__(self)

Definiert die String-Repräsentation des Objekts, wird von print() und str() verwendet

__int__(self)

Definiert die Integer-Repräsentation des Objekts, wird von int() verwendet

__float__(self)

Definiert die Float-Repräsentation des Objekts, wird von float() verwendet

__complex__(self)

Definiert die Complex-Repräsentation des Objekts, wird von complex() verwendet

__add__(self, other), __radd__(self, other)

Definiert das Verhalten des + Operators für Objekte der Klasse

__sub__(self, other), __rsub__(self, other)

Definiert das Verhalten des - Operators für Objekte der Klasse

__mul__(self, other), __rmul__(self, other)

Definiert das Verhalten des * Operators für Objekte der Klasse

__truediv__(self, other), __rtruediv__(self, other)

Definiert das Verhalten des / Operators für Objekte der Klasse

__floordiv__(self, other), __rfloordiv__(self, other)

Definiert das Verhalten des // Operators für Objekte der Klasse

__eq__(self, other), __req__(self, other)

Definiert das Verhalten des == Operators für Objekte der Klasse

__lt__(self, other), __rlt__(self, other)

Definiert das Verhalten des < Operators für Objekte der Klasse

__le__(self, other), __rle__(self, other)

Definiert das Verhalten des <= Operators für Objekte der Klasse

__gt__(self, other), __rgt__(self, other)

Definiert das Verhalten des > Operators für Objekte der Klasse

__ge__(self, other), __rge__(self, other)

Definiert das Verhalten des >= Operators für Objekte der Klasse

__ne__(self, other), __rne__(self, other)

Definiert das Verhalten des != Operators für Objekte der Klasse

__len__(self)

Definiert das Verhalten der len() Funktion für Objekte der Klasse

__getitem__(self, key)

Definiert das Verhalten des Indexzugriffs (z.B. obj[key]) für Objekte der Klasse

__setitem__(self, key, value)

Definiert das Verhalten des Setzens von Werten über Indexzugriff (z.B. obj[key] = value) für Objekte der Klasse

__delitem__(self, key)

Definiert das Verhalten des Löschens von Werten über Indexzugriff (z.B. del obj[key]) für Objekte der Klasse

Beispiel: Arithmetik mit der Bruchklasse#

In der Klasse für Brüche kann die __str__-Methode statt der print-Methode definiert werden, um eine benutzerfreundliche String-Darstellung des Objekts zu ermöglichen. Die anderen magischen Methoden ermöglichen es, Brüche wie normale Zahlen zu behandeln, z.B. durch Addition, Multiplikation oder Vergleich.

class Fraction:
    def __init__(self, numerator, denominator):
        # Make sure that denominator is non-zero and both are integers
        if denominator == 0:
            raise ZeroDivisionError("Denominator cannot be zero")
        if not isinstance(numerator, int) or not isinstance(denominator, int):
            raise TypeError("Numerator and Denominator must be integers")
        self.numerator = numerator
        self.denominator = denominator
        self.simplify()

    # Euclidean algorithm for GCD, used in simplify but can also be used independently of the Object
    @staticmethod
    def gcd(a, b):
            while b:
                a, b = b, a % b
            return a
    
    # Remove common factors from numerator and denominator and make sure that denominator is positive
    def simplify(self):
        common_divisor = Fraction.gcd(abs(self.numerator), abs(self.denominator))
        self.numerator //= common_divisor
        self.denominator //= common_divisor
        if self.denominator < 0:  # keep denominator positive
            self.numerator = -self.numerator
            self.denominator = -self.denominator

    def __str__(self):
        self.simplify()  # ensure fraction is simplified before printing
        if self.denominator == 1:
            return f"{self.numerator}"  # return only numerator if denominator is 1
        else:
            return f"{self.numerator}/{self.denominator}"
    def __int__(self):
        return round(self.numerator / self.denominator) 
    
    def __float__(self):
        return self.numerator / self.denominator
    
    def __add__(self, other):
        if isinstance(other, Fraction):
            new_numerator = self.numerator * other.denominator + other.numerator * self.denominator
            new_denominator = self.denominator * other.denominator
            return Fraction(new_numerator, new_denominator)
        else:
            raise ValueError("Can only add another Fraction")

    
    def __mul__(self, other):
        if isinstance(other, Fraction):
            new_numerator = self.numerator * other.numerator
            new_denominator = self.denominator * other.denominator
            return Fraction(new_numerator, new_denominator)
        else:
            raise ValueError("Can only multiply by another Fraction")

    def __eq__(self, other):
        if isinstance(other, Fraction):
            return self.numerator * other.denominator == self.denominator * other.numerator
        else:
            return False
    

# Testing the Fraction class
f1 = Fraction(4, -8)
f2 = Fraction(10, 5)
print(f"4 divided by -8 simplifies to {f1}, while 10 divided by 5 simplifies to {f2}.")
f3 = Fraction(22,7)
print(f"{f3} rounded to the next integer is {int(f3)}, with error {float(f3) - int(f3)}.")
print(f"Summe von {f1} und {f2} ist {f1 + f2}.")
print(f"Produkt von {f1} und {f2} ist {f1 * f2}.")
print(f"Sind {f1} und {f2} gleich? {'Ja' if f1 == f2 else 'Nein'}")
4 divided by -8 simplifies to -1/2, while 10 divided by 5 simplifies to 2.
22/7 rounded to the next integer is 3, with error 0.1428571428571428.
Summe von -1/2 und 2 ist 3/2.
Produkt von -1/2 und 2 ist -1.
Sind -1/2 und 2 gleich? Nein

Beispiel: Klasse für Kreise mit Index- und Vergleichsoperatoren#

Wir erweiteren die Klasse Circle mit dem Indexoperator [] um schnell auf die Attribute radius und center zugreifen zu können.

  • Zusätzlich implementieren wir Vergleichsoperatoren

  • Wir definieren \(C_1\leq C_2\), wenn der Kreis \(C_1\) in \(C_2\) enthalten ist

    • D.h. der Radius von \(C_1\) plus der Abstand der Mittelpunkte ist kleiner gleich dem Radius von \(C_2\)

  • Wir definieren \(C_1 \geq C_2\), wenn der Kreis \(C_2\) in \(C_1\) enthalten ist

    • D.h. der Radius von \(C_2\) plus der Abstand der Mittelpunkte ist kleiner gleich dem Radius von \(C_1\)

  • Es kann sein, dass weder \(C_1 \leq C_2\) noch \(C_1 \geq C_2\) gilt!

class Circle:
    pi = 3.14159  # static attribute
    def __init__(self, center, radius):
        self.center = center
        self.radius = radius
    def area(self):
        return Circle.pi * self.radius * self.radius
    def circumference(self):
        return 2 * Circle.pi * self.radius
    
    def __getitem__(self, index):
        if index == 0:
            return self.center
        elif index == 1:
            return self.radius
        else:
            raise IndexError("Index out of range. Use 0 for center and 1 for radius.")

    def __setitem__(self, index, value):
        if index == 0:
            self.center = value
        elif index == 1:
            self.radius = value
        else:
            raise IndexError("Index out of range. Use 0 for center and 1 for radius.")
        
    def distance_to_point(self, point):
        return ((point[0] - self.center[0])**2 + (point[1] - self.center[1])**2)**0.5
    
    def __le__(self, other):
        if isinstance(other, Circle):
            return self.distance_to_point(other.center) + self.radius <= other.radius
        else:
            raise ValueError("Can only compare with another Circle")
    def __ge__(self, other):
        if isinstance(other, Circle):
            return self.distance_to_point(other.center) + other.radius <= self.radius
        else:
            raise ValueError("Can only compare with another Circle")
        
C1 = Circle((0,0), 5)
C2 = Circle((3,4), 2)
C3 = Circle((10,10), 3)
print(f"C1 Fläche: {C1.area()}, Umfang: {C1.circumference()}")
print(f"C1 Mittelpunkt: {C1[0]}, Radius: {C1[1]}")
C1[0] = (1,1)
C1[1] = 6
print(f"Neuer C1 Mittelpunkt: {C1[0]}, Neuer Radius: {C1[1]}")
print(f"Ist C2 in C1? {'Ja' if C1 >= C2 else 'Nein'}")
print(f"Ist C1 in C2? {'Ja' if C2 >= C1 else 'Nein'}")
print(f"Ist C3 in C1? {'Ja' if C1 >= C3 else 'Nein'}")
C1 Fläche: 78.53975, Umfang: 31.4159
C1 Mittelpunkt: (0, 0), Radius: 5
Neuer C1 Mittelpunkt: (1, 1), Neuer Radius: 6
Ist C2 in C1? Ja
Ist C1 in C2? Nein
Ist C3 in C1? Nein
# Visualisierung, siehe Kapitel "Visualisierung mit Matplotlib" zur Erklärung
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
circles = [C1, C2, C3]
colors = ['blue', 'green', 'red']
labels = ['C1', 'C2', 'C3']

for circle, color, label in zip(circles, colors, labels):
    circle_patch = plt.Circle(circle.center, circle.radius, color=color, alpha=0.5, label=label)
    ax.add_patch(circle_patch)
    ax.plot(circle.center[0], circle.center[1], 'o', color=color)  # Mark center

ax.set_xlim(-10, 15)
ax.set_ylim(-10, 15)
ax.set_aspect('equal', adjustable='datalim')
plt.legend()
plt.title("Visualisierung der Kreise")
plt.xlabel("x")
plt.ylabel("y")
plt.grid(True)
plt.show()
_images/934d7645aa1506a16afe46066fb32de7b93f2472d4c119a3b3b2699122fcd12f.png