Die Grundidee objektorientierter Programmierung ist, die im Programm benötigten Funktionen und Daten in logisch zusammengehörige Einheiten als Objekte zusammenzufassen um damit die Komplexität des Programms zu reduzieren. Wir haben dann, wenn man so will, nicht mehr ein großes und komplexes Programm, sondern viele kleine, überschaubare und miteinander interagierende Programme.
Ein Objekt ist also ein "Ding", das
Funktionalität bereitstellt, über die
Objekte können konkrete Entitäten, wie etwa einen Gegenstand oder eine Person beschreiben, aber auch abstrakte Dinge, wie etwa einen Vorgang oder ein Konzept.
Wesentlich ist, dass Objekte in sich geschlossene Einheiten aus Daten und Funktionen darstellen, die nach außen nur wenige, klar definierte Schnittstellen anbieten.
Das Konzept der Objektorientierung lässt sich anhand eines konkreten Beispiels am einfachsten verstehen.
Wenn wir ein Bibliotheksverwaltungsprogramm schreiben wollen, können wir
die Bücher z.B. als Liste von Tupeln verwalten. Jeder Listeneintrag
repräsentiert ein Buch und jedes Tupel die zur Beschreibung dieses Buches
benötigten Daten wie Autor
, Titel
, Erscheinungsjahr
usw.
In [ ]:
books = [
("Klein, Bernd", "Einführung in Python", "Hanser", "3", 2017),
("Sweigart, Al", "Automate the Boring Stuff with Python", "No Starch Press", "1", 2015),
("Weigend, Michael", "Python", "mitp", "6., erw. Aufl.", 2016),
("Downey, Allen B.", "Programmieren lernen mit Python", "O'Reilly", "1", 2014)
]
Auch die Bibliotheksbenutzer können wir auf diese Art in einer zweiten Liste verwalten. Statt Tupeln verwenden wir hier Dictionaries:
In [ ]:
users = [
{'firstname': 'Anton',
'lastname': 'Huber',
'address': 'Maygasse 12, 8010 Graz',
'status': 'Student',
'borrowed_books': []
},
{'firstname': 'Anna',
'lastname': 'Schmidt',
'address': 'Rosenberg 5, 8010 Graz',
'status': 'Professor',
'borrowed_books': []
},
]
Für den Fall, dass ein Benutzer ein Buch entlehnen will, könnten wir eine
Funktion entlehne(benutzer, buch)
schreiben. Die Sache scheint einfach.
In [ ]:
def entlehne(user, book):
user["borrowed_books"].append(book)
Die Sache kann schnell komplex werden, wenn wir überprüfen müssen, ob ein Buch entlehnbar ist, welche maximale Entlehndauer ein Benutzer hat, wie viele Bücher er gleichzeitig entlehnen darf usw.
Die objektorientierte Programmierung versucht diese Komplexität zu vermeiden, indem sie Objekte als weitgehend eigenständige Entitäten einführt. Jedes Objekt stellt dabei eine Einheit dar, die in der Lage ist, seine Eigenschaften (Name, Adresse, Status, entlehnte Bücher) selbst zu verwalten.
Darüber hinaus bieten Objekte definierte Schnittstellen, über die (und nur über die!) die Außenwelt mit dem Objekt interagieren kann. Ein Bibliotheksbenutzerobjekt könnte etwas diese Methoden haben:
usw.
Dabei könnte ein Objekt "User" die Methode (Prozedur) borrow_book(book)
haben, in der überprüft wird, ob der Benutzer noch Bücher entlehnen darf (er könnte ja bereits sein Entlehnmaximum errreicht haben oder wegen nicht bezahlter Gebühren gesperrt sein). Das Objekt kümmert sich in der Folge auch um Dinge wie Entlehnfristen, Mahnungen usw. Der Vorteil liegt darin, dass die Programmiererin, die Code schreibt, bei dem ein Buch entlehnt wird, sich keinerlei Gedanken über diese Dinge machen muss. Sie verwendet einfach die Methode borrow_book(book); das Benutzer-Objekt weiß selbst, ob und wie die Entlehnung durchzuführen ist.
Die meisten objektorientierten Programmiersprachen unterscheiden zwischen Klassen, die quasi die Vorlage zur Erstellung eines Objekts darstellen und für die Erzeugung von Objekten zuständig sind, und den eigentlichen Objekten, mit denen gearbeitet wird. Diese Unterscheidung trifft auch Python. Das bedeutet, dass wir, ehe wir ein Objekt erzeugen können, zuerst eine entsprechende Klasse brauchen. Python bringt von sich aus eine Menge von Klassen mit, die wir bisher schon verwendet haben, ohne groß darüber nachzudenken.
In [ ]:
distinct_names = set()
erzeugt ein (leeres) Set-Objekt, genau so wie
In [ ]:
names = []
nichts anderes ist als die Kurzschreibweise für
In [ ]:
names = list()
wodurch eine neues list
-Objekt erzeugt wird.
In [ ]:
type(names)
Klassen definieren also Datentypen.
In [ ]:
class Student:
pass
Sobald wir die Klasse definiert haben, können wir daraus neue Objekte erzeugen:
In [ ]:
hans = Student()
anna = Student()
In [ ]:
hans
In [ ]:
type(hans)
Wir haben also wirklich einen neuen Datentyp Student
geschaffen.
Wir wir bereits gehört haben, kombiniert ein Objekt Eigenschaften und Methoden. Beginnen wir mit den Eingenschaften. In Python (Achtung: das gilt nicht für alle objektorientieren Sprachen!), können wir einem bereits erzeugten Objekt nachträglich Eigenschaften zuweisen, die in der Klasse nicht vorgesehen sind:
In [ ]:
hans.firstname = 'Hans'
anna.lastname = 'Huber'
hans.firstname
In [ ]:
anna.firstname
Hier sehen wir bereits, dass das freie Zuweisen von Eigenschaften an existierende Objekte nicht ganz unproblematisch ist, weil wir dadurch Objekte vom selben Typ erhalten, die u.U. unterschiedliche Eigenschaften haben, was das Konzept eines Typs sabotiert. Wir sollten deshalb besser die Klasse nutzen, um alle benötigten Eigenschaften festzulegen.
__init__()
ist eine spezielle Methode, die, wenn sie in einer Klasse definiert ist,
automatisch unmittelbar nach dem Erzeugen des Objekts aufgerufen wird. Manche nennen __init__()
den Konstruktur der Klasse, was aber technisch gesehen in Python nicht ganz korrekt ist. (Ein Konstruktur erzeugt ein Objekt aus einer Klasse). Wir können uns aber bis auf weiteres __init__()
als eine Art Konstruktor vorstellen.
In [ ]:
class Student:
def __init__(self, firstname, lastname):
self.firstname = firstname
self.lastname = lastname
Innerhalb der __init__()
-Methode weisen wir die übergebenen Werte dem Objekt (referenziert über den Namen self
) zu. Damit werden die Werte Eigenschaftswerte des Objekts.
In [ ]:
hans = Student('Hans', 'Meier')
In [ ]:
hans.firstname
In [ ]:
hans.lastname
Wenn wir eine Klasse mit einer __init__()
-Methode ausstatten, muss ein Objekt mit den entsprechenden Argumenten erzeugt werden. Das ist der Grund warum der folgende Code nicht funktioniert:
In [ ]:
anna = Student()
Da aber die Methode __init__()
nichts anderes ist, als eine dem Objekt zugewiesene Funktion, gilt hier alles, was wir bereits bei Funktionen gelernt haben. Man kann also z.B. Defaultwerte definieren:
In [ ]:
import random
class Student:
def __init__(self, firstname, lastname, matrikelnummer=None):
self.firstname = firstname
self.lastname = lastname
# if matrikelnummer is None, generate it randomly
if matrikelnummer is None:
self.matrikelnummer = '017{}'.format(random.randint(100000, 999999))
else:
self.matrikelnummer = matrikelnummer
In [ ]:
hans = Student('Hans', 'Meier', '017542345')
anna = Student('Anna', 'Huber')
In [ ]:
hans.matrikelnummer
In [ ]:
anna.matrikelnummer
Ich habe oben behauptet, dass Methoden nichts anderes sind als Funktionen, die einem Objekt zugewiesen und nur im Kontext des Objekts verfügbar sind.
In [ ]:
class Rectangle:
def __init__(self, length, width):
self.length = length
self.width = width
def get_area(self):
return self.length * self.width
get_area()
ist eine Funktion, die nicht allgemein überall im Programm verfügbar ist,
In [ ]:
get_area()
sondern nur im Kontext eines Objekts dieser Klasse:
In [ ]:
a_rect = Rectangle(80, 50)
a_rect.get_area()
Sie haben sich vermutlich schon gefragt, was es mit diesem self
auf sich hat, das als erster Parameter einer jeden Methode definiert wird, das aber anscheinend beim Aufruf der Methode nicht angegeben wird:
def get_area(self, length, width):
return self.length * self.width
a_rect = Rectangle(80, 50)
a_rect.get_area()
self
ist nichts anderes als die Referenz auf das jeweilige Objekt. In der Methodendefinition bedeutet das self
, dass die Methode dem jeweiligen Objekt zuzuweisen ist, so wie bei den Eigenschaften (self.length
, self.width
) ein Wert über die self
-Referenz dem jeweilige Objekt zugewiesen wird.