Das Entwerfen einer Klasse bedeutet die Modellierung der benötigten Information. Wie jede Modellbildung impliziert das Abstraktion und Reduktion.
Unter Abstraktion verstehen wir hier das Herausarbeiten von Gemeinsamkeiten und somit das Erkennen von übergeordneten Kategorien. Anna und Hans sind unterschiedliche Individuen, die eine für uns interessante Gemeinsamkeit haben: Sie sind beide Studierende. Indem wir erkannt haben, dass beide Individuen dieser Kategorie oder Klasse zugeordnet werden können (unsere Wahrnehmung tut dies ununterbrochen), haben wir bereits (zumindest implizit) ein Modell Studierende(r) entwickelt. Dieses muss nun für die Überführung in ein Computersystem noch genau formalisiert werden.
Jedes Modell (und eine Klasse ist nichts anderes als ein solches Modell)
stellt eine Vereinfachung dessen dar, was modelliert wird. Für die
Klasse Student
müssen wir also überlegen, welche Daten so ein Modell
beschreiben. Wir könnten ein extrem komplexes Modell entwerfen, in dem
ein Student durch tausende Eigenschaften beschrieben wird, von der Schuhgröße
bis zum bevorzugten Urlaubsland. Die eigentliche Kunst ist jedoch,
sich auf die für den konkreten Anwendungsfall benötigten Daten zu beschränken,
gleichzeitig aber auch keine zu vergessen.
Der Schritt der Modellierung darf nicht unterschätzt werden. In dieser Phase geht es darum, eine klare Vorstellung davon zu bekommen, welche Objekte im Programm benötigt werden, welche Eigenschaften diese haben und wie sie mit ihrer Umwelt interagieren (Methoden).
Fehler in der Planungsphase zu erkennen und zu reparieren ist weniger aufwendig und damit weniger kostenträchtig, als wenn der Modellierungsfehler erst im halbfertigen Programm entdeckt wird.
In der Praxis zeichnet man hier echte "Konstruktionspläne" z.B. in Form von UML-Diagrammen. Diese dienen nicht nur als Hilfe beim Nachdenken über ein Projekt, sondern auch Kommunikationsmittel und zur Dokumentation.
Bei den Unterlagen im Moodle-Kurs gibt es ein eigenes Dokument zum Thema UML (Unified Modeling Language).
In [ ]:
class BankAccount:
"Represents a bank account."
def __init__(self, account_number=None):
"Initialize or create a new account."
self.account_number = ''
self.__balance = 0
self.holder = None
self._transactions = []
if account_number is None:
self._create_new_acconunt()
else:
self.account_number = account_number
self._fetch_account_data(self, account_number)
def get_balance(self):
"Return the actual balance."
return self.__balance
def get_available_amount(self):
"Return the maximum amount which can be withdrawn."
rv = self.__balance + self._calculate_overdraft()
if rv < 0:
rv = 0
return rv
def make_deposit(self, amount, text):
"Add money to this account."
self.__balance += amount
self.transactions.append(Transaction(amount, info=text))
def withdraw(self, amount, text):
"Withdraw amount from this account."
overdraft = self._calculate_overdraft()
if self.__balance + overdraft - amount > 0:
self.__balance -= amount
self.transactions.append(Transaction(amount * -1, info=text))
return True
else:
raise BalanceTooLowException('You max amount is ...')
def transfer_money(self, target_account, amount, text):
"Transfer money from this account to target_account."
overdraft = self._calculate_overdraft()
if self.__balance + overdraft - amount > 0:
# start some complecated procedurce which transfers money
self.transactions.append(Transaction(amount * -1, target=target_account))
else:
raise BalanceTooLowException('You max amount is ...')
def _create_new_account(self):
"Create initial data for new account"
self.holder = input('Enter name of account holder:')
def _fetch_account_data(self, account_number):
"Fetch some data from e.g. a database"
# Missing: the DB code
self.balance = account_data.get_balance()
self.holder = account_data.get_holder()
Das ist nun relativ viel Code. Wesentlich aus der Sicht der Kapselung sind hier zwei Dinge:
Der Kontostand (__balance
) darf von außen nicht direkt verändert werden, sondern nur über bestimmte Methoden:
* `make_deposit()`
* `withdraw()`
* `transfer_money()`
Dadurch wird sichergestellt, dass niemand ungeprüft den Kontostand verändern kann (also z.B. mehr abheben als erlaubt ist). Außerdem können wir in den Methoden Code haben, der z.B. jede Transaktion protokolliert.
Ein Programmierer der mit so einem Account-Objekt interagieren will, braucht über die Funktionsweise des Objekts faktisch nichts zu wissen, sondern muss nur die Methoden kennen, die nach außen verfügbar sind:
* `get_balance()`
* `make_deposit()`
* `withdraw()`
* `transfer_money()`
Sollen 1000 Euro auf das Konto überwiesen werden reicht diese Codezeile
`make_deposit(1000, 'Erfolgsprämie für ...')`
Soll Geld auf ein anderes Konto überwiesen werden, kann dies ebenfalls mit einer Zeile erledigt werden:
`transfer_money('AT01234711223322', 1200, 'Miete für Dezember')`
Um die Details kümmert sich jeweils das Objekt selbst.
Die Kapselung der Funktionalität bietet einen weiteren Vorteil: So lange die Schnittstelle der Methode (d.h. ihr Name, die Parameter und der Rückgabewert) sich nicht verändern, kann der Funktionskörper jederzeit verändert werden, ohne dass Programme, die das Objekt nutzen, umgeschrieben werden müssen. Hier ein Beispiel:
In [1]:
class Product:
def __init__(name, price):
self.name = name
self.price = price
def get_gross_price(self):
"Return gross price (with tax)."
return self.prince + self.price / 100 * 20
Sollte sich die Mehrwertsteuer ändern, kann dies in der Methode get_full_price() geändert werden. Existierender Code, der Objekte vom Typ Product
nutzt, braucht nicht verändert zu werden. Viel wichtiger kann das natürlich werden, wenn sich zum Beispiel die Art verändert, wie Banken Überweisungen durchführen: man ändert den Körper der Methode transfer_money()
, aus der Sicht eines Programms, das Account
-Objekte nutzt, ändert sich nichts.
Unter diesem Begriff versteht man das Prinzip, das von einer Klasse jederzeit eine spezialisierte Unterform abgeleitet werden kann. Es entsteht so eine Hierarchie von Typen. Nehmen wir an, wir wollen für die Universitätsbibliothek eine Benutzerverwaltung schreiben, in der es unterschiedlichen Typen von Benutzern gibt. Zunächst erstellen wir einen Basistyp User
:
In [ ]:
class MaxBorrowingsError(Exception): pass
class BookNotBorrowedException(Exception): pass
class User:
def __init__(self, firstname, lastname):
self.max_books = 10
self.firstname = firstname
self.lastname = lastname
self.borrowed_books = []
def borrow_book(self, book):
if len(self.borrowed_books) < self.max_books:
self.borrowed_books.append(book)
else:
raise MaxBorrowingsError('You have exceeded the number of books you are '
'allowed to borrow.')
def return_book(self, book):
if book in self.borrowed_books:
self.borrowed_books.remove(book)
else:
raise BookNotBorrowedException('You did not borrow this book!')
Ein Student ist ein spezieller User, der zusätzlich noch eine Matrikellnummer hat:
In [ ]:
class Student(User):
def __init__(self, firstname, lastname, matrikelnummer):
self.max_books = 10
self.firstname = firstname
self.lastname = lastname
self.matrikelnummer = matrikelnummer
self.borrowed_books = []
Hier sind 2 Dinge zu beachten:
class Student(User)
legt fest, dass die neue Klasse Student
einen Spezialfall der
Klasse User
darstellt.__init__()
. Alle anderen Methoden erbt Student
von der Klasse User
.
In [ ]:
otto = Student('Otto', 'Huber', '017844556')
otto.borrow_book('A Book')
print(otto.borrowed_books)
otto.return_book('A Book')
print(otto.borrowed_books)
Wir sehen hier, dass die Methoden borrow_books()
und return_books()
zur Verfügung stehen, obwohl wir sie beim Schreiben der Klasse Student
gar nicht definiert haben. Sie kommen von der Basisklasse User
. Dieser Mechanismus ist sehr mächtig, weil wir nur die Teile einer Klasse zu verändern brauchen, die den Spezialfall der Elternklasse manifestieren.
In [ ]:
type(otto)
Wir können auch auf einen bestimmten Typen testen:
In [ ]:
type(otto) is Student
In [ ]:
isinstance(otto, Student)
Mit isinstance()
können wir sogar testen, ob ein Wert einen bestimmten Typus hat, der weiter oben in der Vererbungskette steht:
In [ ]:
isinstance(otto, User)
Da Vererbung immer Spezialisierung bedeutet, ist in unserer Typhierarchie ein Student immer auch ein User.
In [ ]:
In [ ]:
class User2:
def __init__(self, firstname, lastname):
self.max_books = 10
self.firstname = firstname
self.lastname = lastname
self.borrowed_books = []
class Student2(User2):
def __init__(self, firstname, lastname, matrikelnummer):
super().__init__(firstname, lastname)
self.matrikelnummer = matrikelnummer
anna = Student2('Anna', 'Meier', '0175546655')
print(anna.firstname, anna.matrikelnummer)
In [ ]:
import math
class Rectangle:
def __init__(self, length, width):
self.length = length
self.width = width
def get_area(self):
return self.length * self.width
class Circle:
def __init__(self, radius):
self.radius = radius
def get_area(self):
return self.radius ** 2 * math.pi
rect = Rectangle(60, 40)
circ = Circle(25)
print(rect.get_area())
print(circ.get_area())
Das hat den Vorteil, dass wir die Formen in gleicher Weise verwenden können. Um etwas die Gesamtfläche mehrerer geometrischer Formen zu ermitteln können wir das tun:
In [ ]:
figures = [Rectangle(72, 55), Circle(42), Rectangle(22,19)]
sum([fig.get_area() for fig in figures])