| Parameter | Kursinformationen |
|---|---|
| Veranstaltung: | @config.lecture |
| Semester | @config.semester |
| Hochschule: | Technische Universität Freiberg |
| Inhalte: | Objektorientierung in Python |
| Link auf Repository: | https://github.com/TUBAF-IfI-LiaScript/VL_EAVD/blob/master/09_PythonOOP.md |
| Autoren | @author |
Fragen an die heutige Veranstaltung ...
- Wie lassen sich die Konzepte der OOP in Python ausdrücken?
- Welche spezifischen Einschränkungen gibt es dabei?
Klassen werden verwendet, um benutzerdefinierte Datenstrukturen zu erstellen und definieren Funktionen, sogenannte Methoden, die das Verhalten und die Aktionen identifizieren, die ein aus der Klasse erstelltes Objekt mit seinen Daten ausführen kann.
Eine kurze Auffrischung Ihrer Erinnerungen zur objektorientierter Programmierung in C++ ...
#include <iostream>
#include <cmath>
class Rectangle {
private:
float width, height;
public:
Rectangle(float w, float h){
this->width = abs(w); // ensure non-negative width
this->height = abs(h);
}
float area() {return width*height;}
Rectangle operator+=(Rectangle offset) {
float ratio = (offset.area() + this->area()) / this->area();
this->width = ratio * this->width;
return *this;
}
};
int main () {
Rectangle rect_a(3,4);
Rectangle rect_b(1,3);
std::cout << "Fläche a : " << rect_a.area() << "\n";
std::cout << "Fläche b : " << rect_b.area() << "\n";
rect_a += rect_b;
std::cout << "Summe : " << rect_a.area();
return 0;
}@LIA.eval(["main.cpp"], g++ -Wall main.cpp -o a.out, ./a.out)
| Zeile | Bedeutung |
|---|---|
| 4-18 | Definition der Klasse Rectangle (Schablone für Daten, Methoden, Operatoren) |
| 6 | Gekapselte Daten der Klasse, diese sind "von Außen" nicht sichtbar |
| 8 | Konstruktor mit Evaluation der übergebenen Parameter |
| 12 | Methode über den Daten der Klasse |
| 13 | Individueller Operator + mit einer spezifischen Bedeutung |
| 21-26 | Generierung von Objekten mittels Konstruktoraufruf und Parameterübergabe, Methoden- und Operatoraufrufe |
Objektorientierte Programmierung (OOP) ist ein Paradigma, das über die Ideen der Prozeduralen Programmierung hinaus geht. Es definiert Objekte und deren Verhalten. Dabei baut es auf 3 zentralen Grundprinzipien auf:
- Kapselung Objekte kapseln ihre Daten, Operatoren, Methoden usw. sofern diese nicht als "öffentlich" deklariert sind.
Was intern passiert bleibt intern!
- Vererbung Objekte können "Fähigkeiten" an andere, speziellere Objekte weitergeben.
Von wem hat er das denn wohl?
- Polymorphismus Objekte werden durch Kapselung und Vererbung austauschbar!
Was bist denn Du für einer?
Vorteile der objektorientierten Programmierung
- höhere Wartbarkeit durch Abstraktion
- Wiederverwendbarkeit von Code (bessere Wiederverwendbarkeit je kleiner und allgemeiner die Objekte gehalten sind)
- schlanker und übersichtlicher Code durch Vererbung
Warum also nicht immer objektorientiert entwickeln?
OOP verführt ggf. dazu, das eigentliche Problem durch einen aufwändigen Entwurf unnötig zu verkomplizieren. Dabei ist die Entwicklung der Gesamtstruktur eines komplexen Softwareprojektes aus n Objekten eine Kunst und braucht viel Übung! Erst, wenn man entsprechende Regeln kennt und sinnvoll anwendet, zeigen sich die Vorteile des Paradigmas.
In Python ist alles ein Objekt!
... z.B. auch Integer und Floats, die in C++ und vielen anderen Programmiersprachen keine Objekte sind
import inspect
i=5
for name, data in inspect.getmembers(i):
if name == '__builtins__':
continue
print(f'{name} - {repr(data)}')@LIA.eval(["main.py"], none, python3 main.py)
Alle Klassendefinitionen beginnen mit dem Schlüsselwort class, gefolgt vom Namen der Klasse und einem Doppelpunkt. Jeder Code, der unterhalb der Klassendefinition eingerückt ist, wird als Teil des Klassenhauptteils betrachtet.
Analog zu C++ nutzt Python für die Interaktion mit den Klassenelementen eine dot notation.
import inspect
class Dog: # Schlüsselwort "class"
family = "Canidae"
name = "Bello"
age = 5
d = Dog()
print(d.family)
d.name = "Russel"
print(d.name)
for name, data in inspect.getmembers(d):
if name == '__builtins__':
continue
print(f'{name} - {repr(data)}')@LIA.eval(["main.py"], none, python3 main.py)
Aufgabe: Erläutern Sie die Ausgabe folgenden Codes. Wie müssen wir das Ergebnis interpretieren?
import inspect
class Dog:
family = "Canidae"
name = "Bello"
age = 5
d1 = Dog()
print("dog 1:", d1.name, d1.age)
print(d1)
d2 = Dog()
print("dog 2:", d2.name, d2.age)
print(d2)
print(d1 == d2)@LIA.eval(["main.py"], none, python3 main.py)
{{1}}
Antworten:
-
d1 und d2 sind Objekte vom Typ "Hund" mit gleichen Attributen (Name, Alter, Familie), aber es handelt sich trotzdem um verschiedene Objekte (deren Daten an verschiedenen Stellen im Speicher liegen)
-
Gleichheit von zwei Objekten der Klasse Hund wird offenbar nicht als Gleichheit aller Attribute berechnet.
- Man könnte Gleichheit von Hunden auch anders definieren (durch Definition
einer speziellen Methode
__eq__)
- Man könnte Gleichheit von Hunden auch anders definieren (durch Definition
einer speziellen Methode
Zudem fällt auf: Name und Alter sind für individuelle Hunde üblicherweise verschieden, die Familie "Canidae" bezieht sich aber auf alle Hunde. Um dies besser zu modellieren, sollte zwischem Instanzvariablen und Klassenvariablen unterschieden werden ...
Frage: Für welche Aufgaben ist der Konstruktor in einer Klasse verantwortlich?
class Dog:
family = "Canidae" # Klassenvariable
def __init__(self, name, age):
self.name = name # Instanzvariable
self.age = age
i = Dog("Rex", 5)
print(i.name, i.family, i.age)@LIA.eval(["main.py"], none, python3 main.py)
Instanzmethoden sind Funktionen, die innerhalb einer Klasse definiert sind und nur von einer Instanz dieser Klasse aufgerufen werden können. Genau wie bei __init__() ist der erste Parameter einer Instanzmethode immer self.
class Dog:
family = "Canidae"
def __init__(self, name, age):
self.name = name
self.age = age
def makeSound(self): # : nicht vergessen!
print(f"{self.name} says Wuff")
i = Dog("Rex", 5)
i.makeSound()@LIA.eval(["main.py"], none, python3 main.py)
Aufgabe: Schreiben Sie eine Methode, so dass eine Instanz von Dog in Abhängigkeit von ihrem Alter schläft. Recherchieren Sie dazu unter
python delaydie notwendigen Methoden dertimeKlasse.
Wie Sie bereits bei der Inspektion der list, int aber auch der Dog Klasse gesehen haben, existiert eine Zahl von vordefinierten Funktionen - die sogenannten dunder Methods. Das Wort dunder leitet sich von double underscore ab.
| Methode | Typ | implementiert |
|---|---|---|
__init__() |
Konstruktor | |
__str__() |
Methode | Generiert einen String aus den Objektdaten |
| ... | ||
__add__() |
Operator Obj + Obj |
Arithmetische Operation |
| ... | ||
__eq__() |
Operator Obj == Obj |
Logische Operation |
__lt__() |
Operator Obj <= Obj |
|
| ... |
Eine gute Einführung und detailierte Erklärung liefert Link
Python nutzt zwei führende Unterstriche, um Methoden und Variablen als private zu markieren.
class A:
def method_public(self):
print("This is a public method")
def __method_private(self):
print("This is a private method")
obj = A()
obj.method_public()@LIA.eval(["main.py"], none, python3 main.py)
Auf private Methoden einer Klasse kann weder außerhalb der Klasse noch von irgendeiner Basisklasse aus zugegriffen werden kann.
Wie können wir die private Methode überhaupt aufrufen?
Was stört Sie an folgendem Codebeispiel?
class Student:
def __init__(self, fname, lname):
self.firstname = fname
self.lastname = lname
def printname(self):
print("Student -", self.firstname, self.lastname)
class StaffMember:
def __init__(self, fname, lname):
self.firstname = fname
self.lastname = lname
def printname(self):
print("Staff -", self.firstname, self.lastname)
humboldt = Student("Alexander", "von Humboldt")
cotta = StaffMember("Bernhard", "von Cotta")
humboldt.printname()
cotta.printname()@LIA.eval(["main.py"], none, python3 main.py)
Vererbung überträgt das Verhalten einer Basisklasse auf eine abgeleitete Klasse. Dadurch wird redundanter Code gespart.
class Person:
def __init__(self, fname, lname):
self.firstname = fname
self.lastname = lname
def printname(self):
print(self.firstname, self.lastname)
class Student(Person):
pass
class StaffMember(Person):
pass
humboldt = Student("Alexander", "von Humboldt")
cotta = StaffMember("Bernhard", "von Cotta")
humboldt.printname()
cotta.printname()@LIA.eval(["main.py"], none, python3 main.py)
Unterklassen erweitern ihre Oberklasse typischerweise um zusätzliche Attribute und Methoden. Methoden der Unterklasse können Methoden der Oberklasse überschreiben (Method Overriding). Im folgenden Beispiel wird dies anhand von zwei dunder Methoden gezeigt:
-
in der
__init__-Methode (Konstruktor zur Erzeugung von Instanzen) ist der Unterklassen wird jeweils ein weiteres Attribut angelegt. Die Konstruktoren der Unterklassen müssen auch den Konstruktor der Oberklasse aufrufen. -
die
__str()__-Methode liefert eine String-Repräsention des Objekts, die inbesonder auch vonprintgenutzt wird. Die Implementierung in den Unterklassen rufen hier auch die__str__-Methode der Oberklasse auf.
"Unterklassen mit zusätzlichen Attributen"
class Person:
def __init__(self, fname, lname):
self.firstname = fname
self.lastname = lname
def __str__(self):
return f"{self.firstname} {self.lastname}"
class Student(Person):
def __init__(self, fname, lname, id=10000):
super().__init__(fname, lname)
self.student_id = id
def __str__(self):
return "Student: " + super().__str__() + f", ID: {self.student_id}"
class StaffMember(Person):
def __init__(self, fname, lname, id=2000):
super().__init__(fname, lname)
self.staff_id = id
def __str__(self):
return "Staff Member: " + super().__str__() + f", ID: {self.staff_id}"
humboldt = Student("Alexander", "Humboldt")
cotta = StaffMember("Bernhard", "von-Cotta", id=2001)
print(humboldt)
print(cotta)@LIA.eval(["main.py"], none, python3 main.py)
Die Unterscheidung zwischen Instanzvariablen und Klassenvariablen wurde schon oben bei der Klasse Dog angesprochen.
Instanzvariablen (oder Member Variables) sind typischerweise für jedes Objekt unterschiedliche belegt. Beispiele sind etwa der Name von Personen oder Tieren (Alexander, Mary, Fido, ...) oder die Matrikelnummer von Studierenden.
Klassenvariablen beziehen dagegen sich auf die Klasse selbst.
- Der Zugriff auf Klassenvariablen sollte nach dem Schema Klassenname.Klassenvariable erfolgen!
Im folgenden Beispiel wird in der Klasse Student eine Klassenvariable next_available_id zur automatischen Generierung eindeutiger Martikelnummern für neue Studierende genutzt.
class Person:
def __init__(self, fname, lname):
self.firstname = fname
self.lastname = lname
def __str__(self):
return f"{self.firstname} {self.lastname}"
class Student(Person):
next_available_id = 10000
def __init__(self, fname, lname):
super().__init__(fname, lname)
self.student_id = Student.next_available_id
Student.next_available_id += 1
def __str__(self):
return "Student: " + super().__str__() + f", ID: {self.student_id}"
humboldt = Student("Alexander", "Humboldt")
hegeler = Student("Mary", "Hegeler")
print(humboldt)
print(hegeler)
print("Next available student ID:", Student.next_available_id)@LIA.eval(["main.py"], none, python3 main.py)
-
Das Konzept der (Methoden-)Überladung wird in Python nicht nativ unterstützt!
-
in C++ kann es in einer Klasse mehrere Methoden gleichen Namens geben, sofern sich die Typen der Parameter unterscheiden
-
in Python kann es dagegen nur eine Methode mit demselben Namen geben
-
als Konsequenz werden in Python oft Funktionen mit relativ vielen Parametern definiert
-
viele oder oft auch alle Parameter haben Default-Werte, oft
None -
zusätzliche Typannotationen der Parameter verbessern die Lesbarkeit
-
der Aufruf der Methoden erfolgt dann flexibel über Schlüsselwort-Argumente (d.h. durch explizite Angabe des Parameternames)
-
-
class Dog:
def __init__(self,
name:str | None = None,
age:int | None = None,
breed:str | None = None):
self.name = name
self.age = age
self.breed = breed
def __str__(self):
return f"Dog(Name: {self.name}, Age: {self.age}, Breed: {self.breed})"
# Creating Dog instances with different combinations of arguments
d1 = Dog(name = "Buddy", age=3, breed = "Golden Retriever")
d2 = Dog(age = 5, name = "Max")
d3 = Dog(name = "Bella", breed = "Beagle")
d4 = Dog()
print(d1, d2, d3, d4, sep="\n")@LIA.eval(["main.py"], none, python3 main.py)
-
Private ist in Python nicht wirklich private
-
direkter Aufruf privater Methoden über ihren eigentlichen Methodennamen resultiert in einem Fehler
-
private Methoden können jedoch mittels "Name Mangling" aufgerufen werden
-
private Methoden sollte man trotzdem nicht von außerhalb der Klasse aufrufen (schlechter Stil)
-
class A:
def fun(self):
print("This is a public method.",
"It may call a private method but this is none of your business.")
def __fun(self):
print("This is a private method.")
obj = A()
obj.fun()
# obj.__fun() # <- AttributeError
obj._A__fun() # <- Name Mangling "_classname__function"@LIA.eval(["main.py"], none, python3 main.py)
Nehmen wir an, dass wir eine Liste von Vornamen erzeugen wollen. Dabei soll sichergestellt werden, dass diese unabhängig von den Eingaben der Bediener vergleichbar sind. Zudem sollen fehlerhafte Eingaben, die zum Beispiel Zahlen enthalten erkannt und gefiltert werden.
class NameList(list):
def __init__(self):
super().__init__()
def append(self, item):
if isinstance(item, str) and item.isalpha():
super().append(item.lower())
else:
print(f"Cannot add {item} to name list!",
"Expected a string with alphabetic characters only.")
def uniques(self):
# return set(self) # no duplicates, but unordered
return sorted(set(self)) # no duplicates, ordered
friends = NameList()
friends.append("Jannes")
friends.append("linda")
friends.append("Moritz")
friends.append("MORITZ")
friends.append("Linda2") # name with digit is not allowed
friends.append(42) # wrong data type for name lists
print(friends)
print(friends.uniques())@LIA.eval(["main.py"], none, python3 main.py)
Dafür schreiben wir eine abgeleitet Listenklasse mit einer eigenen Implementierung von append().
Aufgabe Erweitern Sie die Implementierung auf die
extend()Methode der Listen.
Zum Vergleich mit dem einführenden C++-Beispiel hier noch eine Python-Implementierung der Rectangle-Klasse in C++. Der +=-Operator wird hier mittels der dunder-Methode __iadd__() (in-place addition) implementiert.
class Rectangle:
def __init__(self, width: float, height: float):
self.width = abs(width) # ensure non-negative width
self.height = abs(height)
def area(self):
return self.width * self.height
def __iadd__(self, offset):
ratio = (offset.area() + self.area()) / self.area()
self.width = ratio * self.width
return self
if __name__ == "__main__":
rect_a = Rectangle(3, 4)
rect_b = Rectangle(1, 3)
print(f"Fläche a : {rect_a.area()}")
print(f"Fläche b : {rect_b.area()}")
rect_a += rect_b
print(f"Summe : {rect_a.area()}")@LIA.eval(["main.py"], none, python3 main.py)
Dataclasses, die es seit Python 3.7 gibt, ermöglichen eine einfache und komfortable Definition von Klassen, die hauptsächlich der Datenhaltung dienen, aber deren Verhalten (durch Definition von Methoden) weniger wichtig ist.
Sie sind in etwa vergleichbar zu structs in C++.
Für Dataclasses wird viel "Boilerplate-Code" automatisch generiert, z.B. die Methoden __init__(), __eq__() und __repr__().
Die automatische Code-Generierung erfolgt durch einen Dekorator @dataclass.
Felder von Datenklassen werden über Typannotationen sowie optionalen Default-Werten definiert (man verwendet den Begriff Feld auch, weil die Felder syntaktisch eher wie Klassenvariablen aussehen, durch den Dekorator aber zu Instanzvariablen gemacht werden; durch den anderen Begriff wird es weniger verwirrend).
from dataclasses import dataclass
@dataclass
class Dog:
name: str | None = None # field 'name' with default value None
age: int | None = None
breed: str | None = None
d1 = Dog(name = "Buddy", age=3, breed = "Golden Retriever")
d2 = Dog(age = 5, name = "Max")
d3 = Dog(name = "Bella", breed = "Beagle")
d4 = Dog(breed = "Beagle", name = "Bella")
d5 = Dog()
print(d1, d2, d3, d4, d5, sep="\n")
print("Is d3 equal to d4?", d3 == d4) # True, as the field values are the same@LIA.eval(["main.py"], none, python3 main.py)
Hinter den Kulissen erzeugt der Dekorator u.a. automatisch einen Konstruktor:
"Automatically generated __init__ method by @dataclass:"
def __init__(self,
name: str | None = None,
age: int | None = None,
breed: str | None = None):
self.name = name
self.age = age
self.breed = breed
Interessant an Dataclasses ist zudem, dass Instanzen entweder als veränderliche (Default) oder unveränderliche Objekte definiert werden können.
-
Erweitert man den Dekorator um den Parameter
frozen=True, dann sind die Instanzen unveränderlich -
Ein Vorteil von unveränderlichen Objekten ist, dass sie Elemente von Sets oder Schlüssel von Dictionaries sein können.
from dataclasses import dataclass
@dataclass(frozen=True) # default: frozen=False
class Dog:
name: str | None = None # field 'name' with default value None
age: int | None = None
breed: str | None = None
d1 = Dog(name = "Buddy", age=3, breed = "Golden Retriever")
d2 = Dog(name = "Bella", breed = "Beagle")
d3 = Dog(breed = "Beagle", name = "Bella")
dogs = {d1, d2, d3} # d2 and d3 are considered equal
print(dogs)@LIA.eval(["main.py"], none, python3 main.py)
Aufgabe Definieren Sie ein Dictionary, in welchem Instanzen der Dataclass Dog als Schlüssel verwendet werden. Experimentieren Sie dabei mit den beiden möglichen Werten den Parameter
frozendes Dekorators (False bzw. True).
Für welche der genannten Grundprinzipien der objektorientierten Programmierung treffen folgende Aussagen zu:
[[Kapselung] [Vererbung] [Polymorphie] ] [( ) ( ) (X) ] Welche konkrete Implementierung der Methode aufgerufen wird, hängt davon ab mit welchem konkreten Objekt sie aufrufen wird. [(X) ( ) ( ) ] Objekte schützen ihre Daten und Methoden sofern diese nicht als "öffentlich" deklariert sind. [( ) (X) ( ) ] Objekte können ihre Daten und Methoden an andere, spezielle Objekte weitergeben.
Mit welchem Schlüsselwort beginnen Klassendefinitionen in Python? [[class]]
{{1}}
Wodurch muss
[_____]ersetzt werden, um den Nachnamen von Studentneuerauszugeben?
class Student:
lastName = "Neuer"
firstName = "Markus"
age = 20
neuer = Student()
print([_____])[[neuer.lastName]]
Wie lauten die Ausgaben foldender Programme?
class Player:
max_health = 100
max_experience = 10
def __init__(self, name, level):
self.name = name
self.level = level
self.health = Player.max_health
self.experience = Player.max_experience
p1 = Player("Peter", 2)
p2 = Player("Frank", 6)
print(p2.level, p2.experience)[[6 10]]
{{1}}
class Player:
max_health = 100
max_experience = 10
def __init__(self, name, level):
self.name = name
self.level = level
self.health = Player.max_health
self.experience = Player.max_experience
def level_up(self):
self.level += 1
self.health = Player.max_health
self.experience = 1
p1 = Player("Peter", 2)
p2 = Player("Frank", 6)
p2.level_up()
print(p2.level, p2.experience)[[7 1]]
{{2}}
Welche dieser Methoden ist eine dunder Method?
[(X)] __sum__
[( )] sum
[( )] _sum
[( )] _sum_
Welche der im folgenden Code aufgeführten Methoden und Variablen sind öffentlich und welche privat?
class Dog:
def bark(self):
print("woof")
def __bark_loud(self):
print("WOOF!")
Fifi = Dog()
Fifi.bark_loud()[[bark()] [__bark_loud()] ]
[(X) ( ) ] öffentlich
[( ) (X) ] privat
{{1}}
Was ist die Ausgabe des oben gezeigten Codes? [( )] woof [( )] WOOF! [(X)] Das Programm wird mit einem Error abgebrochen
{{2}}
Ist es in Python grundsätzlich möglich auch private Methoden auszuführen? [(X)] Ja [( )] Nein
Wordurch muss
[_____]ersetzt werden, um die AusgabeWOOF!zu erzeugen?
class Dog:
def bark(self):
[_____]
def __bark_subtle(self):
print("woof")
def __bark_loud(self):
print("WOOF!")
Fifi = Dog()
Fifi.bark()[[self.__bark_loud()]]
Wodurch muss
[_____]ersetzt werden, um eine neue KlasseAutozu erstellen, die das Verhalten der KlasseFahrzeugerbt?
class Fahrzeug:
def __init__(self, ps):
self.ps = ps
class [_____]:
pass
a1 = Auto(70)[[Auto(Fahrzeug)]]
