9. Klassen¶
Verglichen mit anderen Programmiersprachen, fügt Pythons Klassenmechanismus Klassen mit einem Minimum an neuer Syntax und Semantik zur Sprache hinzu. Er ist eine Mischung der Klassenmechanismen von C++ und Modula-3. Python Klassen bieten alle Standardeigenschaften von objektorientierter Programmierung: Der Vererbungsmechanismus von Klassen erlaubt mehrere Basisklassen, eine abgeleitete Klasse kann jegliche Methoden seiner Basisklasse(n) überschreiben und eine Methode kann die Methode der Basisklasse mit demselben Namen aufrufen. Objekte können beliebig viele Mengen und Arten von Daten haben. Wie es auch bei Modulen der Fall ist, haben auch Klassen die dynamische Natur von Python inne: Sie werden zur Laufzeit erstellt und können auch nach der Erstellung verändert werden.
In der Terminologie von C++ sind class members (inklusive der data member) normalerweise in Python public (Ausnahmen siehe Private Variablen) und alle member functions (Methoden) sind virtual. Wie in Modula-3 gibt es keine Abkürzung zum referenzieren von Attributen eines Objekts aus dessen Methoden heraus: Die Methode wird mit einem expliziten ersten Argument, welches das Objekt repräsentiert, deklariert, und dann implizit beim Aufruf übergeben wird. Wie in Smalltalk sind Klassen selbst Objekte. Das bietet Semantiken zum importieren und umbenennen. Anders als in C++ und Modula-3 können eingebaute Datentypen vom Benutzer als Basisklassen benutzt, das heisst abgeleitet, werden. Außerdem können die meisten eingebauten Operatoren, wie in C++, mit einer besonderen Syntax (arithmetische Operatoren, Indizierung, usw.) für Instanzen der Klasse neu definiert werden.
(Da es keine allgemein anerkannte Terminologie im Bezug auf Klassen gibt, werde ich zwischendurch auf Smalltalk und C++ Begriffe ausweichen. Ich würde lieber Modula-3 Begriffe benutzen, da seine objektorientierte Semantik näher an Python, als an C++ ist, allerdings erwarte ich, dass wenige Leser davon gehört haben.)
9.1. Ein Wort zu Namen und Objekten¶
Objekte haben Individualität und mehrere Namen (in mehreren Gültigkeitsbereichen) können an dasselbe Objekt gebunden werden. In anderen Sprachen wird dies als Aliasing bezeichnet. Das wird meist beim ersten Blick auf Python nicht geschätzt und kann problemlos ignoriert werden, wenn man mit unveränderbaren Datentypen (Zahlen, Zeichenketten, Tupel) arbeitet. Aber Aliasing hat einen möglicherweise überraschenden Effekt auf die Semantik von Pythoncode, der veränderbare Objekte wie Listen, Dictionaries oder die meisten anderen Typen, enthält. Dies kommt normalerweise dem Programm zugute, da sich Aliase in mancher Hinsicht wie Pointer verhalten. Zum Beispiel ist die Übergabe eines Objekts günstig, da von der Implementierung nur ein Pointer übergeben wird. Verändert eine Funktion ein Objekt, das als Argument übergeben wurde, wird der Aufrufende die Veränderung sehen — dies vermeidet den Bedarf an zwei verschiedenen Übergabemechanismen, wie in Pascal.
9.2. Gültigkeitsbereiche und Namensräume in Python¶
Bevor man Klassen überhaupt einführt, muss man über Pythons Regeln im Bezug auf Gültigkeitsbereiche reden. Klassendefinitionen wenden ein paar nette Kniffe bei Namensräumen an und man muss wissen wie Gültigkeitsbereiche und Namensräume funktionieren, um vollkommen zu verstehen was abläuft. Außerdem ist das Wissen hierüber nützlich für jeden fortgeschrittenen Pythonprogrammierer.
Fangen wir mit ein paar Definitionen an.
Ein Namensraum ist eine Zuordnung von Namen zu Objekten. Die meisten
Namensräume sind momentan als Dictionaries implementiert, aber das ist
normalerweise in keinerlei Hinsicht spürbar (außer bei der Performance) und kann
sich in Zukunft ändern. Beispiele für Namensräume sind: Die Menge der
eingebauten Namen (die Funktionen wie abs()
und eingebaute Ausnahmen
enthält), die globalen Namen eines Moduls und die lokalen Namen eines
Funktionsaufrufs. In gewisser Hinsicht bilden auch die Attribute eines Objektes
einen Namensraum. Das Wichtigste, das man über Namensräume wissen muss, ist,
dass es absolut keinen Bezug von Namen in verschiedenen Namensräumen zueinander
gibt. Zum Beispiel können zwei verschiedene Module eine Funktion namens
maximize
definieren, ohne dass es zu einer Verwechslung kommt, denn Benutzer
des Moduls müssen dessen Namen voranstellen.
Nebenbei bemerkt: Ich benutze das Wort Attribut für jeden Namen nach einem
Punkt — zum Beispiel in dem Ausdruck z.real
ist real
ein Attribut des
Objekts z
. Genau genommen sind auch Referenzen zu Namen in Modulen
Attributreferenzen: Im Ausdruck modname.funcname
, ist modname
ein
Modulobjekt und funcname
ein Attribut dessen. In diesem Fall gibt es eine
geradlinige Zuordnung von Modulattributen und globalen Namen, die im Modul
definiert sind: Sie teilen sich denselben Namensraum! [1]
Attribute können schreibgeschützt oder veränderbar sein. In letzterem Fall ist
eine Zuweisung an dieses Attribut möglich. Modulattribute sind veränderbar: Man
kann modname.the_answer = 42
schreiben. Veränderbare Attribute sind
gleichzeitig durch die del
-Anweisung löschbar. Zum Beispiel löscht
del modname.the_answer
das Attribut the_answer
des Objekts namens
modname
.
Namensräume werden zu verschiedenen Zeitpunkten erzeugt und haben verschiedene
Lebenszeiten. Der Namensraum, der die eingebauten Namen enthält, wird beim Start
des Interpreters erzeugt und nie gelöscht. Der globale Namensraum für ein Modul
wird erzeugt, wenn die Moduldefinition eingelesen wird; normalerweise existieren
die Namensräume des Moduls auch solange bis der Interpreter beendet wird. Die
Anweisungen, die auf oberster Ebene vom Interpreter aufgerufen werden, entweder
von einem Skript oder interaktiv gelesen, werden als Teil des Moduls
__main__
behandelt, sodass sie ihren eigenen globalen Namensraum haben.
(Die eingebauten Namen existieren ebenfalls in einem Modul namens
builtins
.)
Der lokale Namensraum einer Funktion wird bei deren Aufruf erstellt und wird gelöscht, wenn sich die Funktion beendet oder eine Ausnahme auslöst, die nicht innerhalb der Funktion behandelt wird. (Eigentlich wäre “vergessen” eine bessere Beschreibung dessen, was passiert.) Natürlich haben auch rekursive Aufrufe ihren jeweiligen lokalen Namensraum.
Ein Gültigkeitsbereich (scope) ist eine Region eines Python-Programms, in der ein Namensraum direkt verfügbar ist, das heisst es einem unqualifiziertem Namen möglich ist einen Namen in diesem Namensraum zu finden.
Auch wenn Gültigkeitsbereiche statisch ermittelt werden, werden sie dynamisch benutzt. An einem beliebigen Zeitpunkt während der Ausführung, gibt es mindestens drei verschachtelte Gültigkeitsbereiche, deren Namensräume direkt verfügbar sind:
- Der innerste Gültigkeitsbereich, der zuerst durchsucht wird und die lokalen Namen enthält;
- der Gültigkeitsbereich mit allen umgebenden Namensräumen (enthält auch die globalen Namen des momentanen Moduls), der vom nächsten umgebenden Namensraum aus durchsucht wird, und nicht-lokale, aber auch nicht-globale Namen enthält;
- der vorletzte Gültigkeitsbereich enthält die globalen Namen des aktuellen Moduls;
- der letzte Gültigkeitsbereich (zuletzt durchsuchte) ist der Namensraum, der die eingebauten Namen enthält.
Wird ein Name als global
deklariert, so gehen alle Referenzen und
Zuweisungen direkt an den mittleren Gültigkeitsbereich, der die globalen Namen
des Moduls enthält. Um Variablen, die außerhalb des innersten
Gültigkeitsbereichs zu finden sind, neu zu binden, kann die
nonlocal
-Anweisung benutzt werden. Falls diese nicht als
nonlocal
deklariert sind, sind diese Variablen schreibgeschützt (ein Versuch
in diese Variablen zu schreiben, würde einfach eine neue lokale Variable im
innersten Gültigkeitsbereich anlegen und die äußere Variable mit demselben Namen
unverändert lassen).
Normalerweise referenziert der lokale Gültigkeitsbereich die lokalen Namen der momentanen Funktion. Außerhalb von Funktionen bezieht sich der lokale Gültigkeitsbereich auf denselben Namensraum wie der globale Gültigkeitsbereich: Den Namensraum des Moduls. Klassendefinitionen stellen einen weiteren Namensraum im lokalen Gültigkeitsbereich dar.
Es ist wichtig zu verstehen, dass die Gültigkeitsbereiche am Text ermittelt werden: Der globale Gültigkeitsbereich einer Funktion, die in einem Modul definiert wird, ist der Namensraum des Moduls, ganz egal wo die Funktion aufgerufen wird. Andererseits wird die tatsächliche Suche nach Namen dynamisch zur Laufzeit durchgeführt — jedoch entwickelt sich die Definition der Sprache hin zu einer statischen Namensauflösung zur Kompilierzeit, deshalb sollte man sich nicht auf die dynamische Namensauflösung verlassen! (In der Tat werden lokale Variablen schon statisch ermittelt.)
Eine besondere Eigenart Pythons ist, dass – wenn keine
global
-Anweisung aktiv ist – Zuweisungen an Namen immer im innersten
Gültigkeitsbereich abgewickelt werden. Zuweisungen kopieren keine
Daten, sondern binden nur Namen an Objekte. Das gleiche gilt für Löschungen: Die
Anweisung del x
entfernt nur die Bindung von x
aus dem Namensraum des
lokalen Gültigkeitsbereichs. In der Tat benutzen alle Operationen, die neue
Namen einführen, den lokalen Gültigkeitsbereich: Im Besonderen binden
import
-Anweisungen und Funktionsdefinitionen das Modul
beziehungsweise den Funktionsnamen im lokalen Gültigkeitsbereich.
Die global
-Anweisung kann benutzt werden, um anzuzeigen, dass
bestimmte Variablen im globalen Gültigkeitsbereich existieren und hier
neu gebunden werden sollen. Die nonlocal
-Anweisung zeigt an, dass
eine bestimmte Variable im umgebenden Gültigkeitsbereich existiert und hier
neu gebunden werden soll.
9.2.1. Beispiel zu Gültigkeitsbereichen und Namensräumen¶
Dies ist ein Beispiel, das zeigt, wie man die verschiedenen Gültigkeitsbereiche
und Namensräume referenziert und wie global
und :keyword`nonlocal`
die Variablenbindung beeinflussen:
def scope_test():
def do_local():
spam = "local spam"
def do_nonlocal():
nonlocal spam
spam = "nonlocal spam"
def do_global():
global spam
spam = "global spam"
spam = "test spam"
do_local()
print("Nach der lokalen Zuweisung:", spam)
do_nonlocal()
print("Nach der nonlocal Zuweisung:", spam)
do_global()
print("Nach der global Zuweisung:", spam)
scope_test()
print("Im globalen Gültigkeitsbereich:", spam)
Die Ausgabe des Beispielcodes ist:
.. code-block:: none
Nach der lokalen Zuweisung: test spam
Nach der nonlocal Zuweisung: nonlocal spam
Nach der global Zuweisung: nonlocal spam
Im globalen Gültigkeitsbereich: global spam
Beachte, dass die lokale Zuweisung (was der Standard ist) die Bindung von
spam in scope_test nicht verändert hat. Die nonlocal
Zuweisung
die Bindung von spam in scope_test und die global
Zuweisung die
Bindung auf Modulebene verändert hat.
Man kann außerdem sehen, dass es keine vorherige Bindung von spam vor der
global
Zuweisung gab.
9.3. Eine erste Betrachtung von Klassen¶
Klassen führen ein kleines bisschen neue Syntax, drei neue Objekttypen und ein wenig neue Semantik ein.
9.3.1. Syntax der Klassendefinition¶
Die einfachste Form einer Klassendefinition sieht so aus:
class ClassName:
<anweisung-1>
.
.
.
<anweisung-N>
Klassendefinitionen müssen wie Funktionsdefinitionen
(def
-Anweisungen) ausgeführt werden, bevor sie irgendwelche
Auswirkungen haben. (Es wäre vorstellbar eine Klassendefinition in einen Zweig
einer if
-Anweisung oder in eine Funktion zu platzieren.)
In der Praxis sind die Anweisungen innerhalb einer Klassendefinition üblicherweise Funktionsdefinitionen, aber andere Anweisungen sind erlaubt und manchmal nützlich — dazu kommen wir später noch. Die Funktionsdefinitionen innerhalb einer Klasse haben normalerweise eine besondere Argumentliste, die von den Aufrufkonventionen für Methoden vorgeschrieben wird — das wird wiederum später erklärt.
Wird eine Klassendefinition betreten, wird ein neuer Namensraum erzeugt und als lokaler Gültigkeitsbereich benutzt — deshalb werden Zuweisungen an lokale Variablen in diesem neuen Namensraum wirksam. Funktionsdefinitionen binden den Namen der neuen Funktion ebenfalls dort.
Wird eine Klassendefinition normal verlassen (indem sie endet), wird ein
Klassenobjekt erstellt. Dies ist im Grunde eine Verpackung um den Inhalt des
Namensraums, der von der Klassendefinition erstellt wurde. Im nächsten Abschnitt
lernen wir mehr darüber. Der ursprüngliche lokale Gültigkeitsbereich (der vor
dem Betreten der Klassendefinition aktiv war) wird wiederhergestellt und das
Klassenobjekt wird in ihm an den Namen, der im Kopf der Klassendefinition
angegeben wurde, gebunden (ClassName
in unserem Beispiel).
9.3.2. Klassenobjekte¶
Klassenobjekte unterstützen zwei Arten von Operationen: Attributreferenzierungen und Instanziierung.
Attributreferenzierungen benutzen die normale Syntax, die für alle
Attributreferenzen in Python benutzt werden: obj.name
. Gültige Attribute
sind alle Namen, die bei der Erzeugung des Klassenobjektes im Namensraum der
Klasse waren. Wenn die Klassendefinition also so aussah:
class MyClass:
"""A simple example class"""
i = 12345
def f(self):
return 'Hallo Welt'
dann sind MyClass.i
und MyClass.f
gültige Attributreferenzen, die eine
Ganzzahl beziehungsweise ein Funktionsobjekt zurückgeben. Zuweisungen an
Klassenattribute sind ebenfalls möglich, sodass man den Wert von MyClass.i
durch Zuweisung verändern kann. __doc__
ist ebenfalls ein gültiges
Attribut, das den Docstring, der zur Klasse gehört, enthält: "A simple example
class"
.
Klassen Instanziierung benutzt die Funktionsnotation. Tu einfach so, als ob das Klassenobjekt eine parameterlose Funktion wäre, die eine neue Instanz der Klasse zurückgibt. Zum Beispiel (im Fall der obigen Klasse):
x = MyClass()
Dies erzeugt eine neue Instanz der Klasse und weist dieses Objekt der lokalen
Variable x
zu.
Die Instanziierungsoperation (“aufrufen” eines Klassenobjekts) erzeugt ein leeres
Objekt. Viele Klassen haben es gerne Instanzobjekte, die auf einen spezifischen
Anfangszustand angepasst wurden, zu erstellen. Deshalb kann eine Klasse eine
spezielle Methode namens __init__()
, wie folgt definieren:
def __init__(self):
self.data = []
Definiert eine Klasse eine __init__()
-Methode, ruft die
Klasseninstanziierung automatisch __init__()
für die neu erstellte
Klasseninstanz auf. So kann in diesem Beispiel eine neue, initialisierte Instanz
durch folgendes bekommen werden:
x = MyClass()
Natürlich kann die __init__()
-Methode Argumente haben, um eine größere
Flexibilität zu erreichen. In diesem Fall werden die, dem
Klasseninstanziierungsoperator übergebenen Argumente an __init__()
weitergereicht. Zum Beispiel:
>>> class Complex:
... def __init__(self, realpart, imagpart):
... self.r = realpart
... self.i = imagpart
...
>>> x = Complex(3.0, -4.5)
>>> x.r, x.i
(3.0, -4.5)
9.3.3. Instanzobjekte¶
Was können wir jetzt mit den Instanzobjekten tun? Die einzigen Operationen, die Instanzobjekte verstehen, sind Attributreferenzierungen. Es gibt zwei Arten gültiger Attribute: Datenattribute und Methoden.
Datenattribute entsprechen “Instanzvariablen” in Smalltalk und “data members”
in C++. Datenattribute müssen nicht deklariert werden; wie lokale Variablen
erwachen sie zum Leben, sobald ihnen zum ersten Mal etwas zugewiesen wird. Zum
Beispiel wird folgender Code, unter der Annahme, dass x
die Instanz von
MyClass
ist, die oben erstellt wurde, den Wert 16
ausgeben, ohne
Spuren zu hinterlassen:
x.counter = 1
while x.counter < 10:
x.counter = x.counter * 2
print(x.counter)
del x.counter
Die andere Art von Instanzattribut ist die Methode. Eine Methode ist eine
Funktion, die zu einem Objekt gehört. (In Python existiert der Begriff Methode
nicht allein für Klasseninstanzen: Andere Objekttypen können genauso Methoden
haben. Zum Beispiel haben Listenobjekte Methoden namens append()
,
insert()
, remove()
, sort()
, und so weiter. Jedoch benutzen wir
in der folgenden Diskussion den Begriff Methode ausschliesslich im Sinne von
Methoden von Klasseninstanzobjekten, sofern nichts anderes angegeben ist.
Ob ein Attribut eine gültige Methode ist, hängt von der Klasse ab. Per
Definition definieren alle Attribute, die ein Funktionsobjekt sind, ein
entsprechendes Methodenobjekt für seine Instanz. Deshalb ist in unserem Beispiel
x.f
eine gültige Methodenreferenz, da MyClass.f
eine Funktion ist, aber
x.i
ist keine, da MyClass.i
es nicht ist. x.f
ist aber nicht
dasselbe wie MyClass.f
— es ist ein Methodenobjekt und kein
Funktionsobjekt.
9.3.4. Methodenobjekte¶
Üblicherweise wird eine Methode gemäß seiner Bindung aufgerufen:
x.f()
Im MyClass
Beispiel wird dies die Zeichenkette 'Hallo Welt'
ausgeben. Jedoch ist es nicht notwendig eine Methode direkt aufzurufen: x.f
ist ein Methodenobjekt und kann weg gespeichert werden und später wieder
aufgerufen werden. Zum Beispiel:
xf = x.f
while True:
print(xf())
Das wird bis zum Ende der Zeit Hallo Welt
ausgeben.
Was passiert genau, wenn eine Methode aufgerufen wird? Du hast vielleicht
bemerkt, dass x.f()
oben ohne Argument aufgerufen wurde, obwohl in der
Funktionsdefinition für f()
ein Argument festgelegt wurde. Was ist mit
diesem Argument passiert? Natürlich verursacht Python eine Ausnahme, wenn eine
Funktion, die ein Argument benötigt ohne aufgerufen wird — auch wenn das
Argument eigentlich gar nicht genutzt wird ...
Tatsächlich, wie du vielleicht schon erraten hast, ist die Besonderheit bei
Methoden, dass das Objekt als erstes Argument der Funktion übergeben wird. In
unserem Beispiel ist der Aufruf x.f()
das genaue äquivalent von
MyClass.f(x)
. Im Allgemeinen ist der Aufruf einer Methode mit n Argumenten
äquivalent zum Aufruf der entsprechenden Funktion mit einer Argumentliste, die
durch das Einfügen des Objekts der Methode vor das erste Argument erzeugt wird.
Verstehst du immernoch nicht, wie Methoden funktionieren, hilft vielleicht ein Blick auf die Implementierung, um die Dinge zu klären. Wenn ein Instanzattribut referenziert wird, das kein Datenattribut ist, wird seine Klasse durchsucht. Bezeichnet der Name ein gültiges Klassenattribut, das eine Funktion ist, wird ein Methodenobjekt erzeugt, indem (Zeiger zu) Instanzobjekt und Funktionsobjekt zu einem abstrakten Objekt verschmolzen werden: Dies ist das Methodenobjekt. Wird das Methodenobjekt mit einer Argumentliste aufgerufen, wird es wieder entpackt, eine neue Argumentliste aus dem Instanzobjekt und der ursprünglichen Argumentliste erzeugt und das Funktionsobjekt mit dieser neuen Argumentliste aufgerufen.
9.4. Beiläufige Anmerkungen¶
Datenattribute überschreiben Methodenattribute desselben Namens. Um zufällige Namenskonflikte zu vermeiden, die zu schwer auffindbaren Fehlern in großen Programmen führen, ist es sinnvoll sich auf irgendeine Konvention zu verständigen, die das Risiko solcher Konflikte vermindern. Mögliche Konventionen beinhalten das Großschreiben von Methodennamen, das Voranstellen von kleinen eindeutigen Zeichenketten (vielleicht auch nur ein Unterstrich) bei Datenattributen oder das Benutzen von Verben bei Methodennamen und Nomen bei Datenattributen.
Datenattribute können von Methoden, genauso wie von normalen Benutzern (“clients”) eines Objektes referenziert werden. In anderen Worten: Klassen sind nicht benutzbar, um reine abstrakte Datentypen (“abstract data types”) zu implementieren. In Wirklichkeit, gibt es in Python keine Möglichkeit um Datenkapselung (data hiding) zu erzwingen — alles basiert auf Konventionen. (Auf der anderen Seite kann die Python-Implementierung, in C geschrieben, Implementationsdetails komplett verstecken und den Zugriff auf ein Objekt kontrollieren, wenn das nötig ist; das kann von in C geschriebenen Python-Erweiterungen ebenfalls benutzt werden.)
Clients sollten Datenattribute mit Bedacht nutzen, denn sie könnten Invarianten kaputt machen, die von Methoden verwaltet werden, indem sie auf deren Datenattributen herumtrampeln. Man sollte beachten, dass Clients zu ihrem eigenen Instanzobjekt Datenattribute hinzufügen können, ohne die Gültigkeit der Methoden zu gefährden, sofern Namenskonflikte vermieden werden — auch hier kann eine Bennenungskonvention viele Kopfschmerzen ersparen.
Es gibt keine Abkürzung, um Datenattribute (oder andere Methoden!) innerhalb von Methoden zu referenzieren. Meiner Meinung verhilft das Methoden zu besserer Lesbarkeit: Man läuft keine Gefahr, lokale und Instanzvariablen zu verwechseln, wenn man eine Methode überfliegt.
Oft wird das erste Argument einer Methode self
genannt. Dies ist nichts
anderes als eine Konvention: Der Name self
hat absolut keine spezielle
Bedeutung für Python. Aber beachte: Hälst du dich nicht an die Konvention, kann
dein Code schwerer lesbar für andere Python-Programmierer sein und es ist auch
vorstellbar, dass ein Klassenbrowser (class browser) sich auf diese
Konvention verlässt.
Jedes Funktionsobjekt, das ein Klassenattribut ist, definiert eine Methode für Instanzen dieser Klasse. Es ist nicht nötig, dass die Funktionsdefinition im Text innerhalb der Klassendefinition ist: Die Zuweisung eines Funktionsobjektes an eine lokale Variable innerhalb der Klasse ist ebenfalls in Ordnung. Zum Beispiel:
# Funktionsdefintion außerhalb der Klasse
def f1(self, x, y):
return min(x, x+y)
class C:
f = f1
def g(self):
return 'Hallo Welt'
h = g
f
, g
und h
sind jetzt alle Attribute der Klasse C
, die
Funktionsobjekte referenzieren und somit sind sie auch alle Methoden der
Instanzen von C
— h
ist dabei gleichbedeutend mit g
. Beachte
aber, dass diese Praxis nur dazu dient einen Leser des Programms zu verwirren.
Methoden können auch andere Methoden aufrufen, indem sie das Methodenattribut
des Arguments self
benutzen:
class Bag:
def __init__(self):
self.data = []
def add(self, x):
self.data.append(x)
def addtwice(self, x):
self.add(x)
self.add(x)
Methoden können globale Namen genauso wie normale Funktionen referenzieren. Der globale Gültigkeitsbereich der Methode ist das Modul, das die Klassendefinition enthält. (Eine Klasse selbst wird nie als globaler Gültigkeitsbereich benutzt.) Während man selten einen guten Grund dafür hat globale Daten zu benutzen, gibt es viele berechtigte Verwendungen des globalen Gültigkeitsbereichs: Zum einen können Funktionen und Module, die in den globalen Gültigkeitsbereich importiert werden, genauso wie Funktionen und Klassen die darin definiert werden, von der Methode benutzt werden. Normalerweise ist die Klasse, die die Methode enthält, selbst in diesem globalen Gültigkeitsbereich definiert und im nächsten Abschnitt werden wir ein paar gute Gründe entdecken, warum eine Methode die eigene Klasse referenzieren wollte.
Jeder Wert ist ein Objekt und hat deshalb eine Klasse (auch type genannt).
Es wird als Objekt.__class__
abgelegt.
9.5. Vererbung¶
Natürlich verdient ein Sprachmerkmal nicht den Namen “Klasse”, wenn es nicht Vererbung unterstützt. Die Syntax für eine abgeleitete Klassendefinition sieht so aus:
class DerivedClassName(BaseClassName):
<statement-1>
.
.
.
<statement-N>
Der Name BaseClassName
muss innerhalb des Gültigkeitsbereichs, der die
abgeleitete Klassendefinition enthält, definiert sein. Anstelle eines
Basisklassennamens sind auch andere willkürliche Ausdrücke erlaubt. Dies kann
beispielsweise nützlich sein, wenn die Basisklasse in einem anderen Modul
definiert ist:
class DerivedClassName(modname.BaseClassName):
Die Ausführung einer abgeleiteten Klassendefinition läuft genauso wie bei einer Basisklasse ab. Bei der Erzeugung des Klassenobjekts, wird sich der Basisklasse erinnert. Dies wird zum Auflösen der Attributsreferenzen benutzt: Wird ein angefordertes Attribut nicht innerhalb der Klasse gefunden, so wird in der Basisklasse weitergesucht. Diese Regel wird rekursiv angewandt, wenn die Basisklasse selbst von einer anderen Klasse abgeleitet wird.
Es gibt nichts besonderes an der Instanziierung von abgeleiteten Klassen:
DerivedClassName
erzeugt eine neue Instanz der Klasse. Methodenreferenzen
werden wie folgt aufgelöst: Das entsprechende Klassenattribut wird durchsucht,
falls nötig bis zum Ende der Basisklassenkette hinab und die Methodenreferenz
ist gültig, wenn es ein Funktionsobjekt bereithält.
Abgeleitete Klassen können Methoden ihrer Basisklassen überschreiben. Da
Methoden keine besonderen Privilegien beim Aufrufen anderer Methoden desselben
Objekts haben, kann eine Methode einer Basisklasse, die eine andere Methode, die
in derselben Basisklasse definiert wird, aufruft, beim Aufruf einer Methode der
abgeleiteten Klasse landen, die sie überschreibt. (Für C++-Programmierer: Alle
Methoden in Python sind im Grunde virtual
.)
Eine überschreibende Methode in einer abgeleiteten Klasse wird in der Tat eher
die Methode der Basisklasse mit demselben Namen erweitern, statt einfach nur zu
ersetzen. Es gibt einen einfachen Weg die Basisklassenmethode direkt aufzurufen:
Einfach BaseClassName.methodname(self, arguments)
aufrufen. Das ist
gelegentlich auch für Clients nützlich. (Beachte, dass dies nur funktioniert,
wenn die Basisklasse als BaseClassName
im globalen Gültigkeitsbereich
zugänglich ist.)
Python hat zwei eingebaute Funktionen, die mit Vererbung zusammenarbeiten:
- Man benutzt
isinstance()
um den Typ eines Objekts zu überprüfen:isinstance(obj, int)
ist nur dannTrue
, wennobj.__class__
vom Typint
oder einer davon abgeleiteten Klasse ist. - Man benutzt
issubclass()
um Klassenvererbung zu überprüfen:issubclass(bool, int)
istTrue
, dabool
eine vonint
abgeleitete Klasse ist. Jedoch istissubclass(float, int)
False
, dafloat
keine vonint
abgeleitete Klasse ist.
9.5.1. Mehrfachvererbung¶
Python unterstützt auch eine Form der Mehrfachvererbung. Eine Klassendefinition mit mehreren Basisklassen sieht so aus:
class DerivedClassName(Base1, Base2, Base3):
<statement-1>
.
.
.
<statement-N>
Für die meisten Zwecke, im einfachsten Fall, kann man sich die Suche nach
geerbten Attributen von einer Elternklasse so vorstellen: Zuerst in die Tiefe
(depth-first), von links nach rechts (left-to-right), wobei nicht zweimal in
derselben Klasse gesucht wird, wenn sich die Klassenhierarchie dort überlappt.
Deshalb wird, wenn ein Attribut nicht in DerivedClassName
gefunden
wird, danach in Base1
gesucht, dann (rekursiv) in den Basisklassen von
Base1
und wenn es dort nicht gefunden wurde, wird in Base2
gesucht, und so weiter.
In Wirklichkeit ist es ein wenig komplexer als das, denn die Reihenfolge der
Methodenauflösung (method resolution order - MRO) wird dynamisch verändert, um
zusammenwirkende Aufrufe von super()
zu unterstützen. Dieser Ansatz wird
in manchen anderen Sprachen als call-next-method (Aufruf der nächsten Methode)
bekannt und ist mächtiger als der super
-Aufruf, den es in Sprachen mit
einfacher Vererbung gibt.
Es ist nötig dynamisch zu ordnen, da alle Fälle von Mehrfachvererbung eine oder
mehrere Diamantbeziehungen aufweisen (bei der auf mindestens eine der
Elternklassen durch mehrere Pfade von der untersten Klasse aus zugegriffen
werden kann). Zum Beispiel erben alle Klassen von object
und so stellt
jeder Fall von Mehrfachvererbung mehrere Wege bereit, um object
zu erreichen. Um zu verhindern, dass auf die Basisklassen mehr als einmal
zugegriffen werden kann, linearisiert der dynamische Algorithmus die
Suchreihenfolge, sodass die Ordnung von links nach rechts, die in jeder Klasse
festgelegt wird, jede Elternklasse nur einmal aufruft und zwar monoton (in der
Bedeutung, dass eine Klasse geerbt werden kann, ohne das die Rangfolge seiner Eltern
berührt wird). Zusammengenommen machen diese Eigenschaften es möglich
verlässliche und erweiterbare Klassen mit Mehrfachvererbung zu entwerfen. Für
Details, siehe http://www.python.org/download/releases/2.3/mro/.
9.6. Private Variablen¶
“Private” Instanzvariablen, die nur innerhalb des Objekts zugänglich sind, gibt
es in Python nicht. Jedoch gibt es eine Konvention, die im meisten Python-Code
befolgt wird: Ein Name, der mit einem Unterstrich beginnt (z.B. _spam
)
sollte als nicht-öffentlicher Teil der API behandelt werden (egal ob es eine
Funktion, eine Methode oder ein Datenattribut ist). Es sollte als
Implementierungsdetails behandelt werden, das sich unangekündigt ändern kann.
Da es eine sinnvolle Verwendung für klassen-private Attribute gibt, um
Namenskonflikte mit Namen, die von Unterklassen definiert werden zu vermeiden,
gibt es eine begrenzte Unterstützung für so einen Mechanismus: name
mangling (Namensersetzung). Jeder Bezeichner der Form __spam
(mindestens
zwei führende Unterstriche, höchstens ein folgender) wird im Text durch
_classname__spam
ersetzt, wobei classname
der Name der aktuellen Klasse
(ohne eventuelle führende Unterstriche) ist. Die Ersetzung geschieht ohne
Rücksicht auf die syntaktische Position des Bezeichners, sofern er innerhalb der
Definition der Klasse steht.
Namensersetzung ist hilfreich, um Unterklassen zu ermöglichen Methoden zu überschreiben, ohne dabei Methodenaufrufe innerhalb der Klasse zu stören. Zum Beispiel:
class Mapping:
def __init__(self, iterable):
self.items_list = []
self.__update(iterable)
def update(self, iterable):
for item in iterable:
self.items_list.append(item)
__update = update # private Kopie der ursprünglichen update() Methode
class MappingSubclass(Mapping):
def update(self, keys, values):
# erstellt update() mit neuer Signatur
# macht aber __init__() nicht kaputt
for item in zip(keys, values):
self.items_list.append(item)
Beachte, dass die Ersetzungsregeln vor allem dazu gedacht sind, Unfälle zu vermeiden; es ist immernoch möglich auf einen solchen als privat gekennzeichneten Namen von aussen zuzugreifen und ihn auch zu verändern. Das kann in manchen Umständen sogar nützlich sein, beispielsweise in einem Debugger.
Beachte, dass Code, der von exec()
oder eval()
ausgeführt wird, den
Klassennamen der aufrufenden Klasse nicht als die aktuelle Klasse ansieht. Dies
ähnelt dem Effekt der global
-Anweisung, der ebenfalls sehr beschränkt
auf den Code ist, der zusammen byte-kompiliert wird. Die gleiche Begrenzung
gilt für getattr()
, setattr()
und delattr()
, sowie den direkten
Zugriff auf __dict__
.
9.7. Kleinkram¶
Manchmal ist es nützlich einen Datentyp zu haben, der sich ähnlich dem
record
in Pascal oder dem “struct” in C verhält und ein Container für ein
paar Daten ist. Hier bietet sich eine leere Klassendefinition an:
class Employee:
pass
john = Employee() # Eine leere Arbeitnehmerakte anlegen
# Die Akte ausfüllen
john.name = 'John Doe'
john.dept = 'Computerraum'
john.salary = 1000
Einem Stück Python-Code, der einen bestimmten abstrakten Datentyp erwartet, kann
stattdessen oft eine Klasse übergeben werden, die die Methoden dieses Datentyps
emuliert. Wenn man zum Beispiel eine Funktion hat, die Daten aus einem
Dateiobjekt formatiert, kann man eine Klasse mit den Methoden read()
und
readline()
definieren, die die Daten stattdessen aus einem
Zeichenkettenpuffer bekommt, und als Argument übergeben.
Methodenobjekte der Instanz haben auch Attribute: m.__self__
ist das
Instanzobjekt mit der Methode m()
und m.__func__
ist das entsprechende
Funktionsobjekt der Methode.
9.8. Ausnahmen sind auch Klassen¶
Benutzerdefinierte Ausnahmen werden auch durch Klassen gekennzeichnet. Durch die Nutzung dieses Mechanismus ist es möglich erweiterbare Hierarchien von Ausnahmen zu erstellen.
Es gibt zwei neue (semantisch) gültige Varianten der
raise
-Anweisung:
raise Klasse
raise Instanz
In der ersten Variante muss Class
eine Instanz von type
oder einer
davon abgeleiteten Klasse sein und ist eine Abkürzung für:
raise Klasse()
Die in einem except
-Satz angegebene Klasse fängt Ausnahmen dann ab,
wenn sie Instanzen derselben Klasse sind oder von dieser abgeleitet wurden,
nicht jedoch andersrum — der mit einer abgeleiteten Klasse angegebene
except
-Satz fängt nicht die Basisklasse ab. Zum Beispiel gibt der
folgende Code B, C, D in dieser Reihenfolge aus:
class B(Exception):
pass
class C(B):
pass
class D(C):
pass
for cls in [B, C, D]:
try:
raise cls()
except D:
print("D")
except C:
print("C")
except B:
print("B")
Beachte, dass B, B, B ausgegeben wird, wenn man die Reihenfolge umdreht, das
heisst zuerst except B
, da der erste zutreffende except
-Satz
ausgelöst wird.
Wenn eine Fehlermeldung wegen einer unbehandelten Ausnahme ausgegeben wird, wird
der Name der Klasse, danach ein Doppelpunkt und ein Leerzeichen und schliesslich
die Instanz mit Hilfe der eingebauten Funktion str()
zu einer Zeichenkette
umgewandelt ausgegeben.
9.9. Iteratoren¶
Mittlerweile hast du wahrscheinlich bemerkt, dass man über die meisten
Containerobjekte mit Hilfe von for
iterieren kann:
for element in [1, 2, 3]:
print(element)
for element in (1, 2, 3):
print(element)
for key in {'eins':1, 'zwei':2}:
print(key)
for char in "123":
print(char)
for line in open("meinedatei.txt"):
print(line)
Diese Art des Zugriffs ist klar, präzise und praktisch. Der Gebrauch von
Iteratoren durchdringt und vereinheitlicht Python. Hinter den Kulissen ruft die
for
-Anweisung iter()
für das Containerobjekt auf. Die Funktion
gibt ein Iteratorobjekt zurück, das die Methode __next__()
definiert,
die auf die Elemente des Containers nacheinander zugreift. Gibt es keine
Elemente mehr, verursacht __next__()
eine StopIteration
-Ausnahme,
die der for
-Schleife mitteilt, dass sie sich beenden soll. Man kann
auch die __next__()
-Methode mit Hilfe der eingebauten Funktion
next()
aufrufen. Folgendes Beispiel demonstriert, wie alles funktioniert.
>>> s = 'abc'
>>> it = iter(s)
>>> it
<iterator object at 0x00A1DB50>
>>> next(it)
'a'
>>> next(it)
'b'
>>> next(it)
'c'
>>> next(it)
Traceback (most recent call last):
File "<stdin>", line 1, in ?
next(it)
StopIteration
Kennt man die Mechanismen hinter dem Iterator-Protokoll, ist es einfach das
Verhalten von Iteratoren eigenen Klassen hinzuzufügen. Man definiert eine
__iter__()
-Methode, die ein Objekt mit einer __next__()
-Methode
zurückgibt. Definiert die Klasse __next__()
, kann __iter__()
einfach
self
zurückgeben:
class Reverse:
"""Iterator for looping over a sequence backwards."""
def __init__(self, data):
self.data = data
self.index = len(data)
def __iter__(self):
return self
def __next__(self):
if self.index == 0:
raise StopIteration
self.index = self.index - 1
return self.data[self.index]
>>> rev = Reverse('spam')
>>> iter(rev)
<__main__.Reverse object at 0x00A1DB50>
>>> for char in rev:
... print(char)
...
m
a
p
s
9.10. Generatoren¶
Generatoren (generator) sind eine einfache aber mächtige Möglichkeit um
Iteratoren zu erzeugen. Generatoren werden wie normale Funktionen geschrieben,
benutzen aber yield
, um Daten zurückzugeben. Jedes Mal wenn
next()
aufgerufen wird, fährt der Generator an der Stelle fort, an der er
zuletzt verlassen wurde (der Generator merkt sich dabei die Werte aller
Variablen und welche Anweisung zuletzt ausgeführt wurde). Das nachfolgende
Beispiel zeigt wie einfach die Erstellung von Generatoren ist:
def reverse(data):
for index in range(len(data)-1, -1, -1):
yield data[index]
>>> for char in reverse('golf'):
... print(char)
...
f
l
o
g
Alles, was mit Generatoren möglich ist, kann ebenso (wie im vorigen Abschnitt
dargestellt) mit Klassen-basierten Iteratoren, umgesetzt werden. Generatoren
erlauben jedoch eine kompaktere Schreibweise, da die Methoden __iter__()
und __next__()
automatisch erstellt werden.
Des weiteren werden die lokalen Variablen und der Ausführungsstand automatisch
zwischen den Aufrufen gespeichert. Das macht das Schreiben der Funktion einfacher
und verständlicher als ein Ansatz, der mit Instanzvariablen wie self.index
oder self.data
arbeitet.
Generatoren werfen automatisch StopIteration
, wenn sie terminieren.
Zusammengenommen ermöglichen diese Features die Erstellung von Iteratoren mit
einem Aufwand, der nicht größer als die Erstellung einer normalen Funktion ist.
9.11. Generator Ausdrücke¶
Manche einfachen Generatoren können prägnant als Ausdrücke mit Hilfe einer Syntax geschrieben werden, die der von List Comprehensions ähnlich ist, jedoch mit runden, statt eckigen Klammern. Diese Ausdrücke sind für Situationen gedacht, in denen der Generator gleich von der umgebenden Funktion genutzt wird. Generator Ausdrücke sind kompakter, aber auch nicht so flexibel wie ganze Generatordefinitionen und neigen dazu speicherschonender als die entsprechenden List Comprehensions zu sein.
Beispiele:
>>> sum(i*i for i in range(10)) # Summe der Quadrate
285
>>> xvec = [10, 20, 30]
>>> yvec = [7, 5, 3]
>>> sum(x*y for x,y in zip(xvec, yvec)) # Skalarprodukt
260
>>> from math import pi, sin
>>> sine_table = {x: sin(x*pi/180) for x in range(0, 91)}
>>> unique_words = set(word for line in page for word in line.split())
>>> valedictorian = max((student.gpa, student.name) for student in graduates)
>>> data = 'golf'
>>> list(data[i] for i in range(len(data)-1, -1, -1))
['f', 'l', 'o', 'g']
Fußnoten
[1] | Bis auf eine Ausnahme: Modulobjekte haben ein geheimes, schreibgeschützes
Attribut namens __dict__ , das das Dictionary darstellt, mit dem der
Namensraum des Modules implementiert wird; der Name __dict__` ist ein
Attribut, aber kein globaler Name. Offensichtlich ist dessen Benutzung eine
Verletzung der Abstraktion der Namensraumimplementation und sollte deshalb
auf Verwendungen wie die eines Post-Mortem-Debuggers reduziert werden. |