Objektorientierte Programmierung 1: Grundlagen

Was ist objektorientierte Programmierung?

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

  • in der Lage ist, Daten zu speichern, die dieses "Ding" beschreiben (Eigenschaften)
  • Funktionalität bereitstellt, über die

    • die eigenen Eigenschaften (Daten) verändert werden
    • das Ding mit anderen "Dingen" (Objekten) interagieren kann

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.

Ein Beispiel

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:

  • borrow_book(book)
  • return_book(book)
  • get_borrowed_books()

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.

Klassen und Objekte

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.

Eigene Klassen

Eine eigene Klasse zu schreiben ist grundsätzlich einfach:


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.

Eigenschaften eines Objekts

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.

Die init() Methode

__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

Methoden

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()

self

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.

Übung

Schreiben Sie ein Objekt Buch. Überlegen Sie, welche Eigenschaften gebraucht werden und definieren Sie diese entsprechend. Schreiben Sie auch eine Methode get_citation(), die die Eigenschaften des Buchs in einer Form zurückgibt, wie sie diese z.B. in einer Fußnote verwenden würden.