<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Objektorientierte Programmierung 1
Step1:  Auch die
Bibliotheksbenutzer können wir auf diese Art in einer zweiten Liste verwalten. Statt Tupeln verwenden wir hier
Dictionaries
Step2:  Für den Fall, dass ein Benutzer ein Buch entlehnen will, könnten wir eine
Funktion entlehne(benutzer, buch) schreiben. Die Sache scheint einfach.
Step3:  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
Step4:  erzeugt ein (leeres) Set-Objekt, genau so wie
Step5:  nichts anderes ist als die Kurzschreibweise für
Step6:  wodurch eine neues list-Objekt erzeugt wird.
Step7:  Klassen definieren also Datentypen.
Eigene Klassen
Eine eigene Klasse zu schreiben ist grundsätzlich einfach
Step8:  Sobald wir die Klasse definiert haben, können wir daraus neue Objekte erzeugen
Step9:  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
Step10:  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.
Step11:  Innerhalb der __init__()-Methode weisen wir die übergebenen Werte dem Objekt (referenziert über den Namen self) zu. Damit werden die Werte Eigenschaftswerte des Objekts.
Step12:  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
Step13:  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
Step14:  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.
Step15:  get_area() ist eine Funktion, die nicht allgemein überall im Programm verfügbar ist,
Step16:  sondern nur im Kontext eines Objekts dieser Klasse
<ASSISTANT_TASK:>
Python Code:
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)
]
Explanation: 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.
End of explanation
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': []
    },
]
Explanation: Auch die
Bibliotheksbenutzer können wir auf diese Art in einer zweiten Liste verwalten. Statt Tupeln verwenden wir hier
Dictionaries:
End of explanation
def entlehne(user, book):
    user["borrowed_books"].append(book)
Explanation: Für den Fall, dass ein Benutzer ein Buch entlehnen will, könnten wir eine
Funktion entlehne(benutzer, buch) schreiben. Die Sache scheint einfach.
End of explanation
distinct_names = set()
Explanation: 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.
End of explanation
names = [] 
Explanation: erzeugt ein (leeres) Set-Objekt, genau so wie
End of explanation
names = list()
Explanation: nichts anderes ist als die Kurzschreibweise für
End of explanation
type(names)
Explanation: wodurch eine neues list-Objekt erzeugt wird.
End of explanation
class Student:
    pass
Explanation: Klassen definieren also Datentypen.
Eigene Klassen
Eine eigene Klasse zu schreiben ist grundsätzlich einfach:
End of explanation
hans = Student()
anna = Student()
hans
type(hans)
Explanation: Sobald wir die Klasse definiert haben, können wir daraus neue Objekte erzeugen:
End of explanation
hans.firstname = 'Hans'
anna.lastname = 'Huber'
hans.firstname
anna.firstname
Explanation: 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:
End of explanation
class Student:
    
    def __init__(self, firstname, lastname):
        self.firstname = firstname
        self.lastname = lastname
Explanation: 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.
End of explanation
hans = Student('Hans', 'Meier')
hans.firstname
hans.lastname
Explanation: Innerhalb der __init__()-Methode weisen wir die übergebenen Werte dem Objekt (referenziert über den Namen self) zu. Damit werden die Werte Eigenschaftswerte des Objekts.
End of explanation
anna = Student()
Explanation: 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:
End of explanation
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
hans = Student('Hans', 'Meier', '017542345')
anna = Student('Anna', 'Huber')
hans.matrikelnummer
anna.matrikelnummer
Explanation: 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:
End of explanation
class Rectangle:
    
    def __init__(self, length, width):
        self.length = length
        self.width = width
        
    def get_area(self):
        return self.length * self.width
Explanation: 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.
End of explanation
get_area()
Explanation: get_area() ist eine Funktion, die nicht allgemein überall im Programm verfügbar ist,
End of explanation
a_rect = Rectangle(80, 50)
a_rect.get_area()
Explanation: sondern nur im Kontext eines Objekts dieser Klasse:
End of explanation
<END_TASK>