Spezialmethoden und geschicktes Vererben von Klassenmethoden vereinfachen das Programmieren mit Python drastisch. Unser Workshop zeigt, welche Tricks die Skriptsprache auf Lager hat.
Im ersten Teil der Python-Workshops (LinuxUser 09/2006) ging es um den interaktiven Interpreter, Datentypen und Ablaufstrukturen. Der zweite Teil (LinuxUser 10/2006) behandelte das Aufteilen von Code in Funktionen, Module und Pakete. Der dritte Teil (LinuxUser 11/2006) befasste sich mit Fehlern und Ausnahmen und bot einen Einstieg in das objektorientierte Programmieren, das dieser Artikel vertieft.
Im dritten Teil des Workshops hat sich durch das Bearbeiten des Textes eine sinnentstellende Aussage in Bezug auf überladene Methoden ergeben: Überladene Methoden (gleicher Name, aber unterschiedliche Parameterlisten) wie in C++ oder Java gibt es in Python nicht. Man erreicht eine ähnliche Funktionalität, indem man den auszuführenden Code nach den im Aufruf verwendeten Parameternamen auswählt. Alternativ kann man prüfen, ob ein übergebener Parameter eine bestimmte Schnittstelle unterstützt. Die direkte Nachbildung der Typunterscheidung mit der Funktion isinstance ist nicht zu empfehlen, da sie das Duck Typing aushebelt.
Arg abstrakt
Wie in einigen anderen Programmiersprachen gibt es auch in Python das Prinzip einer abstrakten Klasse. Von dieser sollte es keine Instanzen geben. Die abstrakte Basisklasse enthält nur allgemeine (abstrakte) Definitionen; erst die abgeleiteten Klassen definieren sinnvolles, verwendbares Verhalten. Listing 1 zeigt ein Beispiel.
import math
class Form(object):
def flaeche(self):
raise NotImplementedError("in abgeleiteter Klasse definieren")
class Quadrat(Form):
def __init__(self, seitenlaenge=0.0):
self.seitenlaenge = seitenlaenge
def flaeche(self):
return self.seitenlaenge * self.seitenlaenge
class Kreis(Form):
def __init__(self, radius=0.0):
self.radius = radius
def flaeche(self):
return math.pi * self.radius * self.radius
# ggf. weitere Form-Klassen
# …
def gesamtflaeche(formen):
# "generator comprehension"
flaechen = (form.flaeche() for form in formen)
return sum(flaechen, 0.0)
if __name__ == '__main__':
q = Quadrat(10)
k = Kreis(5)
# Ausgabe 178.53981634
print gesamtflaeche([q, k])
Die abstrakte Klasse Form (Zeilen 3 bis 5) definiert die Methode flaeche nur als Platzhalter. Obwohl es in Python nicht zwingend notwendig ist, eine abstrakte Methode wie Form.flaeche zu notieren, dient es manchmal als Dokumentation; die Methode löst lediglich eine Ausnahme vom Typ NotImplementedError aus (Zeile 5). In den abgeleiteten Klassen Quadrat und Kreis (Zeilen 7 bis 19) sind die konkreten Arten der Flächenberechnung bekannt, und flaeche ist jeweils implementierbar.
Da alle Formen eine Methode flaeche haben, berechnet die Funktion gesamtflaeche (Zeilen 24 bis 27) auf einfache Weise die Summe der Flächen, die die Sequenz formen enthält.
Duck Typing
Beim Bearbeiten der Objekte in der Sequenz formen greift ein Prinzip namens Duck Typing. Der Ursprung dieses seltsamen Namens ist der Satz “If it looks like a duck and quacks like a duck, it must be a duck.” (deutsch: Wenn es wie eine Ente aussieht und wie eine Ente quakt, muss es eine Ente sein.) Es gibt keine Typdefinitionen für die Parameter wie in C++ oder Java – also keine Vorschrift, die vorgibt, dass die Klassen der Objekte in formen von der gleichen Basisklasse abstammen müssen. Deshalb dürfen Sie Quadrat und Kreis direkt von object ableiten; die abstrakte Klasse Form ist nicht nötig.
Duck Typing ist in Python sehr gebräuchlich. Ein anderes Beispiel sind dateiartige Objekte, die bestimmte Methoden von Dateiobjekten besitzen. So dürfen Sie beispielsweise an eine Funktion, die die Methoden read, readline und readlines eines Dateiobjekts erwartet, auch ein Objekt vom Typ StringIO[1] übergeben. Das ist sinnvoll, wenn die Daten, die Sie an die Funktion durchreichen, nicht aus einem Dateisystem stammen. Listing 2 zeigt ein entsprechendes Beispiel.
#! /usr/bin/env python
def nummerierte_datei(dateiobjekt):
"""
Eine Generatorfunktion, die die Zeilen aus der Datei
file_object mit dreistelligen Zeilennummern versieht.
"""
zeilennummer = 1
while True:
try:
zeile = dateiobjekt.next()
except StopIteration:
raise
yield "%03d %s" % (zeilennummer, zeile)
zeilennummer += 1
def ausgabe_nummerierte_datei(dateiobjekt):
for zeile in nummerierte_datei(dateiobjekt):
print zeile,
if __name__ == '__main__':
programmdatei = open("nummerierte_datei.py")
ausgabe_nummerierte_datei(programmdatei)
programmdatei.close()
print
# dies sei im Programm generierter Text
text = """\
Dies ist ein mehrzeiliger
Text, der in die Funktion
ausgabe_nummerierte_datei
gesteckt wird."""
import StringIO
textdatei = StringIO.StringIO(text)
ausgabe_nummerierte_datei(textdatei)
textdatei.close()
Bei nummerierte_datei (Zeilen 3 bis 15) handelt es sich um eine Generatorfunktion, wie im zweiten Teil des Python-Kurses beschrieben. Die voranzustellenden Zeilennummern sollen mit 001 beginnen, deshalb bekommt die Variable zeilennummer in Zeile 8 den Startwert 1. Nun durchläuft das Programm bis auf Widerruf (siehe While-Bedingung) die durch dateiobjekt beschriebene Datei. Jede gelesene Zeile erzeugt in Programmzeile 11 eine neue Zeichenkette, die in Codezeile 14 eine Zeilennummer vorangestellt bekommt.
Finden sich keine weiteren Zeilen in der Datei, löst die Codezeile 11 die Ausnahme StopIteration aus, die die Zeile 13 einfach nach oben durchreicht. Dadurch bricht die For-Schleife korrekt ab, die in ausgabe_nummerierte_datei (Programmzeilen 17 bis 19) über das Ergebnis von nummerierte_datei iteriert.
Natürlich wäre es denkbar, mit einer Funktion zu arbeiten, die die zu nummerierenden Zeilen als Sequenz annimmt. Das setzt allerdings voraus, dass das Skript die ganze Datei auf einmal einliest, was bei großen Dateien problematisch wäre.
Der Testcode in den Zeilen 22 bis 37 verwendet zwei dateiartige Objekte, um die Zeilen daraus zu lesen und sie nummeriert auszugeben. In den Programmzeilen 23 bis 25 ist es eine gewöhnliche Datei, nämlich das Programm selbst. Die Zeilen 28 bis 37 erzeugen dagegen ein dateiartiges Objekt direkt aus einer Zeichenkette. Dieses StringIO-Objekt besitzt, wie auch ein echtes Dateiobjekt, eine Methode next, die in Zeile 11 zum Einsatz kommt. Listing 3 zeigt die Programmausgabe, wobei die Ausgabezeilen 6 bis 32 fehlen.
001 #! /usr/bin/env python 002 003 def nummerierte_datei(dateiobjekt): 004 """ 005 Eine Generatorfunktion, die die Zeilen aus der Datei . . . 033 wird.""" 034 import StringIO 035 textdatei = StringIO.StringIO(text) 036 ausgabe_nummerierte_datei(textdatei) 037 textdatei.close() 001 Dies ist ein mehrzeiliger 002 Text, der in die Funktion 003 ausgabe_nummerierte_datei 004 gesteckt wird."""
Dieser Code setzt nicht voraus, dass sich Pythons Dateiklasse und StringIO von der gleichen Basisklasse ableiten. Es reicht, eine bestimmte Schnittstelle zu definieren (hier die Methode next) und eine Ausnahme StopIteration am Ende der Datei zu erzeugen.
Spezielle Methoden
In Python existieren etliche Methoden mit speziellen Namen. Zum einen definieren Sie damit Operatoren wie + oder * für Objekte einer Klasse neu oder nehmen Einfluss auf einige Python-Funktionen und -Anweisungen damit beeinflussen. Im Wesentlichen gibt es folgende Gruppen von speziellen Python-Methoden:
- Objekte erzeugen und zerstören
- Formatieren von Objekten
- Aufruf von Objekten
- Objektvergleiche
- Container-Objekte (analog zu Listen oder Dictionaries)
- numerische Typen
- Attributzugriff
Die folgenden Beispiele zeigen, wie Sie die Methoden aus den genannten Gruppen in der Praxis einsetzen. Es gibt noch weitaus mehr Methoden, deren ausführliche Beschreibung Sie im Netz [2] finden. Denken Sie daran: Sie brauchen nicht alle Methoden aus einer Gruppe zu implementieren, sondern nur die, die Sie zum Lösen einer Aufgabe benötigen. Vermeiden Sie aufgeblähte Schnittstellen – reduzieren Sie die Komplexität von Klassen auf das, was Sie tatsächlich zum Lösen des Problems brauchen. Das erspart Ihnen Arbeit, und sie haben Ihr Problem schneller gelöst.
Erzeugen und Löschen
Eine dieser Methoden, den Konstruktor __init__, haben Sie bereits kennengelernt. So etwas wie das konzeptionelle Gegenteil stellt die Methode __del__ dar, die ein Objekt wegräumt, wenn es nicht mehr erreichbar ist. Beachten Sie, dass die Anweisung del nicht automatisch die __del__-Methode aufruft.
Formatierung von Objekten
Eine oft verwendete Methode ist __str__. Damit steuern Sie, wie die eingebaute Funktion str ein Objekt formatiert, beziehungsweise wie die Anweisung print das Objekt ausgibt (Listing 4).
class C(object):
def __init__(self, x):
self.x = x
def __str__(self):
return "Wert von x: %s" % self.x
x = C(6)
# gibt aus: "Wert von x: 6"
print x
Die Methode __str__ dient üblicherweise dazu, eine schöne Ausgabe für Endanwender zu erzeugen. Damit verwandt ist __repr__, allerdings dient die Ausgabe dieser Methode eher der Fehlersuche durch Programmierer. Die Methode __unicode__ sollte im Gegensatz zu __str__ ein Unicode-Objekt zurückliefern.
Aufruf
Sie erzeugen ein Objekt, indem Sie dessen Klasse wie eine Funktion aufrufen. Mitunter ist es sinnvoll, das spezielle Objekt wie eine Funktion aufzurufen. Dazu eignet sich die Methode __call__. Ein Beispiel:
class Vektor(object):
def __init__(self, x, y):
self.x = x
self.y = y
def __call__(self):
return (self.x, self.y)
v = Vektor(1, 3)
# gibt (1, 3) aus
print v()
Die Methode __call__ verarbeitet, wie andere Funktionen und Methoden auch, beliebige Argumente.
Vergleiche
Möchten Sie zwei Objekte einer Klasse auf eine Weise vergleichen, die nur eine Sortierreihenfolge definiert, steht Ihnen dazu die Methode __cmp__ zur verfügung. Je nachdem, ob der linke Wert kleiner, gleich oder größer als der rechte ist, liefert __cmp__ eine Zahl kleiner, gleich oder größer als Null zurück. Als Beispiel ordnet die folgende Klasse zweidimensionale Vektoren nach ihrem Absolutbetrag:
import math
class Vektor(object):
def __init__(self, x, y):
self.x = x
self.y = y
def __abs__(self):
return math.sqrt(
self.x 2 + self.y 2)
def __cmp__(self, rechts):
return abs(self) - \
abs(rechts)
a = Vektor(1, 2)
b = Vektor(3, -4)
c = Vektor(2, 1)
print a < b # True
print a == b # False
print a == c # True
print b > c # True
Die Methode __cmp__ verwendet hier die eingebaute Funktion abs, die auf die Methode __abs__ aufsetzt. In Python 2.1 kamen sogenannte Rich Comparisons wie __eq__, __lt__ und __ge__ hinzu [3]. Diese Methoden liefern beliebige Objekte zurück.
Falls in eine Klasse keine der Methoden __cmp__, __eq__ oder __ne__ definiert, die eine Reihenfolge festlegen, vergleicht der Interpreter Objekte dieser Klasse anhand ihrer Id (die von der eingebauten Funktion id ermittelt).
Container-Objekte
Python kennt einige Datentypen, zum Beispiel Tupel oder Dictionaries, die wiederum andere Objekte aufnehmen. Solche zusammengesetzten Objekte heißen Container (Behälter). Python erlaubt es, eigene Container zu definieren, die Sequenzen (wie Listen) oder Mappings (wie Dictionaries) nachempfunden sind. Die sinnvollerweise zu definierenden Methoden sind in [4] dokumentiert.
Falls Sie sich fragen, wozu Sie neben den vielseitigen Python-Datentypen eigene Container definieren sollten, schauen Sie sich das Modul shelve[5] an. Sets (Mengen) waren in früheren Python-Versionen übrigens auch als Python-Klassen definiert.
Numerische Typen
Es existieren zahlreiche Methoden [6], die Ihnen dabei helfen, neue numerische Datentypen (wie Pythons eingebaute Typen int, complex, aber auch in Modulen definierte wie datetime.timedelta) zu erzeugen. Die Beispiel-Klasse in Listing 5 enthält diverse Methoden zum Verarbeiten von zweidimensionalen Vektoren.
# coding: iso-8859-1
import math
class Vektor(object):
def __init__(self, x=0.0, y=0.0):
self.x = float(x)
self.y = float(y)
self._cls = self.__class__
def __pos__(self):
return self._cls(self.x, self.y)
def __neg__(self):
return -1.0 * self
def __add__(self, rechte_seite):
return self._cls(self.x+rechte_seite.x, self.y+rechte_seite.y)
def __sub__(self, rechte_seite):
return self + (-rechte_seite)
def __mul__(self, rechte_seite):
try:
rechte_seite.x
except AttributeError:
# rechte_seite ist ein Skalar
skalar = rechte_seite
return self._cls(skalar * self.x, skalar * self.y)
else:
# rechte_seite ist ein Vektor
return self.x*rechte_seite.x + self.y*rechte_seite.y
def __rmul__(self, linke_seite):
return self * linke_seite
def __abs__(self):
return math.sqrt(self * self)
def __repr__(self):
return "Vektor(%g, %g)" % (self.x, self.y)
def __str__(self):
return "(%g, %g)" % (self.x, self.y)
Die Klasse greift in möglichst vielen Methoden auf die Hilfe von anderen Methoden der Klasse zurück. Zum Beispiel subtrahiert die Klasse zwei Vektoren, indem zum ersten Vektor der negierte zweite Vektor hinzukommt. Durch diesen Ansatz sparen Sie viel redundanten Code, wodurch weniger Fehler auftreten.
Die Methode __init__ in den Zeilen 6 bis 9 erlaubt die optionale Angabe der x- und y-Koordinaten. Sind diese nicht vorhanden, setzt die Methode diese auf den Defaultwert 0. Das Zuweisen an self._cls spart weiter unten etwas Schreibarbeit.
Die Methode __pos__ (Zeile 11) liefert eine Kopie des Vektors. Die Verwendung von self._cls bzw. self.__class__ sorgt dafür, dass die Methode, angewandt auf abgeleitete Klassen, wiederum ein Objekt der abgeleiteten Klasse liefert.
Analog zu __pos__ liefert __neg__ (Zeile 14) eine negativ genommene Kopie des Vektors. Dazu wird der Vektor mit -1 multipliziert. Die Methode __add__ (Programmzeile 17) addiert zwei Vektoren. Die Implementation zeigt, dass sich die Summe jeweils aus den Summen der x- und der y-Komponenten ergibt.
Mit __sub__ (Zeile 20) zieht zwei Vektoren voneinander ab. Das geht mit wenig Code: Bei der Addition des ersten Vektor zählt die Methode einfach den Zweite mit umgekehrtem Vorzeichen hinzu. Die eigentliche Logik steckt in dem Methode __add__. Schreiben Sie weniger Code, schleichen sich auch weniger Fehler ein, und es fällt weniger Aufwand beim Testen an.
Die Multiplikationsmethode __mul__ (Zeilen 23 bis 32) kommt in zwei Varianten daher. Mathematisch ist es möglich, einen Vektor mit einem Skalar, also einer einfachen Zahl, zu multiplizieren. Zusätzlich gibt es das so genannte Skalarprodukt, das die Summe der Produkte der x- beziehungsweise y-Komponenten ist.
Um zu unterscheiden, ob die rechte Seite des Multiplikationsoperators ein Skalar oder ein Vektor ist, prüft die Try-Anweisung, ob der Wert ein Attribut x besitzt. Falls nicht, handelt es sich um einen Skalar und die Methode löst einen AttributeError aus. Float-Objekte haben kein Attribut x – Objekte vom Typ Vektor dagegen schon, weil die Klasse sie so definiert.
Die Fehlerbehandlung (Codezeilen 27 bis 29) multipliziert entsprechend die Komponenten des Vektors mit dem Skalar; ein neuer Vektor entsteht. Der Else-Zweig behandelt den Fall zweier Vektoren und gibt das Skalarprodukt zurück.
Die Programmzeilen 34 und 35 zeigen die Methode __rmul__. Diese läuft ab, wenn der andere Operand auf der linken Seite des Multiplikationszeichens steht. Bei der Addition beziehungsweise Subtraktion war das unnötig, weil links immer ein Vektor steht, und damit der richtige Aufruf von __add__ oder __sub__ gesichert ist. Bei der Multiplikation wäre dagegen links auch ein Skalar möglich.
Der Absolutbetrag eines Vektors errechnet sich mit Hilfe des Skalarprodukts des Vektors mit sich selbst (Zeile 37). Dabei hilft die Quadratwurzel-Funktion aus dem Modul math. Die restlichen Methoden der Klasse, __repr__ und __str__, kümmern sich um das Formatieren von Vektor-Objekten, beispielsweise bei der Ausgabe mit print.
Attributzugriff
Weisen Sie einer Variablen ein Objekt zu, verknüpft Python lediglich das Objekt auf der rechten Seite mit dem Namen auf der linken Seite (siehe erster Teil des Kurses). Steht auf der linken Seite dagegen eine Klasse mit Attribut (Klasseninstanz.Attribut), so besteht die Möglichkeit, den Zugriff abzufangen und besonders zu behandeln. Auf die gleiche Weise beeinflussen Sie lesende Zugriffe auf das Attribut und das Löschen (durch del).
Vor allem bei mehreren Attributen, die Sie ähnlich behandeln möchten, empfiehlt sich der Einsatz der Methoden __getattr__, __setattr__ und __delattr__, um den lesenden, schreibenden oder löschenden Attributzugriff zu steuern. Das Beispiel in Listing 6 demonstriert die Handhabung von Attributzugriffen anhand einfacher Stellvertreter-Objekte, die Attributzugriffe anzeigen und auf das eigentliche Objekt self._obj “umleiten”.
class Proxy(object):
def __init__(self, obj):
self.__dict__['_obj'] = obj
def __getattr__(self, name):
print "Lese Attribut", name
return getattr(self._obj, name)
def __setattr__(self, name, wert):
print "Setze Attribut %s auf %s" % (name, wert)
setattr(self._obj, name, wert)
def __delattr__(self, name):
print "Loesche Attribut", name
delattr(self._obj, name)
def __call__(self):
return self._obj
Der Konstruktor in den Zeilen 2 und 3 setzt das Attribut self._obj direkt über das Objekt-Dictionary self.__dict__. Der Grund für die vordergründig komplizierte Technik folgt weiter unten bei der __setattr__-Methode. Hier sei nur gesagt, dass self.__dict__ den Namensraum des Objekts self als Dictionary zugänglich macht, genau so wie die eingebaute Funktion globals() das für den Namensraum eines Moduls tut.
Die Methode __getattr__ (Zeilen 5 bis 7) kommt immer dann zum Einsatz, wenn das gesuchte Attribut nicht im Suchpfad des Objektnamensraums vorhanden ist. Die Definition gibt mit einer Print-Anweisung aus, welches Attribut Sie angefordert haben, und holt das gewünschte Attribut aus dem enthaltenen Objekt self._obj.
Die Methode __setattr__ (Zeilen 9 bis 11) funktioniert ähnlich: Sie beschreibt mittels einer Print-Anweisung den Attributzugriff und leitet diesen an das Objekt self._obj weiter. Die Methode __setattr__ tritt bei jedem zu setzenden Attribut in Aktion, unabhängig davon, ob es bereits im Namensraum des Objekts vorhanden ist.
Lautete nun die Zuweisung im Konstruktor self._obj = obj, würde __setattr__ aufgerufen. In der Definition in Zeile 11 ruft self._obj implizit __getattr__ auf, worin in Zeile 7 wiederum __getattr__ aufgerufen würde. Dadurch käme es zu einer unendlichen Rekursion. Indem in Zeile 3 der Konstruktor den Namensraum des Objekts direkt verändert, den Aufruf von __setattr__ also umgeht, verhindert er die Rekursion.
Die __delattr__-Methode wiederum ist für das Löschen eines Attributs zuständig. Auch dieser Zugriff wirkt auf das enthaltene Objekt self._obj. Ein Aufruf von __call__ (ab Zeile 17) gibt das gekapselte Objekt zurück. Listing 7 zeigt ein Anwendungsbeispiel für die Proxy-Klasse im interaktiven Interpreter.
>>> import proxy >>> class C(object): … def methode(self, argument): … print argument … >>> x = C() >>> p = proxy.Proxy(x) >>> p.methode(7) Lese Attribut methode 7 >>> p.a = 1 Setze Attribut a auf 1 >>> print p.a Lese Attribut a 1 >>> print x.a # direkter Zugriff auf x 1 >>> print p().a # direkter Zugriff auf x 1 >>> del p.a Loesche Attribut a >>> print x.a Traceback (most recent call last): File "<stdin>", line 1, in ? AttributeError: 'C' object has no attribute 'a'
Seit Python 2.2 gibt es außerdem so genannte Deskriptoren [7], die Zugriffe auf einzelne Attribute regeln. Deren Programmierung ist allerdings etwas komplizierter.
[1] StringIO-Modul: http://docs.python.org/lib/module-StringIO.html
[2] Spezielle Methoden: http://docs.python.org/ref/specialnames.html
[3] Rich Comparisons: siehe http://docs.python.org/ref/customization.html
[4] Container-Methoden: http://docs.python.org/ref/sequence-types.html
[5] shelve-Modul: http://docs.python.org/lib/module-shelve.html
[6] Numerische Typen: http://docs.python.org/ref/numeric-types.html
[7] Deskriptoren: http://users.rcn.com/python/download/Descriptor.htm





