9. Klassen¶
Klassen bieten die Möglichkeit, Daten und Funktionalitäten zusammenzufassen. Das Erstellen einer neuen Klasse erzeugt einen neuen Typ von Objekt, was es ermöglicht, neue Instanzen dieses Typs zu erstellen. Jede Klasseninstanz kann über Attribute verfügen, um ihren Zustand zu verwalten. Klasseninstanzen können auch über Methoden (definiert durch ihre Klasse) verfügen, um ihren Zustand zu ändern.
Im Vergleich zu anderen Programmiersprachen fügt der Klassenmechanismus von Python Klassen mit einem Minimum an neuer Syntax und Semantik hinzu. Er ist eine Mischung aus den Klassenmechanismen von C++ und Modula-3. Python-Klassen bieten alle Standardfunktionen der objektorientierten Programmierung: Der Vererbungungsmechanismus erlaubt mehrere Basisklassen, eine abgeleitete Klasse kann jede Methode ihrer Basisklasse(n) überschreiben, und eine Methode kann die gleichnamige Methode einer Basisklasse aufrufen. Objekte können beliebige Mengen und Arten von Daten enthalten. Wie auch Module zeichnen sich Klassen durch die dynamische Natur von Python aus: Sie werden zur Laufzeit erzeugt und können nach ihrer Erstellung weiter verändert werden.
In der C++-Terminologie sind Klassenmitglieder (einschließlich der Datenelemente) normalerweise public (Ausnahmen siehe unten Private Variablen), und alle Elementfunktionen sind virtual. Wie in Modula-3 gibt es keine Kurzschreibweisen für den Zugriff auf die Elemente eines Objekts aus dessen Methoden: Die Methodenfunktion wird mit einem expliziten ersten Argument deklariert, das das Objekt darstellt und beim Aufruf implizit übergeben wird. Wie in Smalltalk sind Klassen selbst Objekte. Dies bietet eine Semantik zum Importieren und Umbenennen. Anders als in C++ und Modula-3 können eingebaute Typen als Basisklassen für Erweiterungen durch den Benutzer verwendet werden. Zudem können, wie in C++, die meisten eingebauten Operatoren mit spezieller Syntax (arithmetische Operatoren, Indizierung usw.) für Klasseninstanzen neu definiert werden.
(Da eine allgemein akzeptierte Terminologie für Klassen fehlt, werde ich gelegentlich Begriffe aus Smalltalk und C++ verwenden. Ich würde Begriffe aus Modula-3 verwenden, da dessen objektorientierte Semantik der von Python näher kommt als die von C++, aber ich nehme an, dass nur wenige Leser davon gehört haben.)
9.1. Ein Wort zu Namen und Objekten¶
Objekte besitzen eine eigene Identität, und mehrere Namen (in verschiedenen Gültigkeitsbereichen) können an dasselbe Objekt gebunden sein. Dies ist in anderen Sprachen als Aliasing bekannt. Auf den ersten Blick wird dies bei Python meist nicht wahrgenommen und kann beim Umgang mit unveränderlichen Datentypen (Zahlen, Zeichenketten, Tupel) getrost ignoriert werden. Aliasing hat jedoch eine unter Umständen überraschende Wirkung auf die Semantik von Python-Code, der veränderliche Objekte wie Listen, Dictionaries und die meisten anderen Typen enthält. Dies wird gewöhnlich zum Vorteil des Programms genutzt, da sich Aliase in mancher Hinsicht wie Zeiger verhalten. Beispielsweise ist die Übergabe eines Objekts günstig, da von der Implementierung nur ein Zeiger übergeben wird; und wenn eine Funktion ein als Argument übergebenes Objekt verändert, sieht der Aufrufer diese Änderung — was zwei verschiedene Mechanismen zur Argumentübergabe wie in Pascal überflüssig macht.
9.2. Python-Gültigkeitsbereiche und Namensräume¶
Bevor wir Klassen einführen, muss ich dir zuerst etwas über Pythons Gültigkeitsbereiche (Scopes) erzählen. Klassendefinitionen wenden einige Raffinessen bei Namensräumen an, und du musst wissen, wie Gültigkeitsbereiche und Namensräume funktionieren, um vollständig zu verstehen, was passiert. Nebenbei bemerkt ist das Wissen über dieses Thema für jeden fortgeschrittenen Python-Programmierer nützlich.
Beginnen wir mit einigen Definitionen.
Ein Namensraum ist eine Zuordnung von Namen zu Objekten. Die meisten Namensräume sind derzeit als Python-Dictionaries implementiert, was aber normalerweise in keiner Weise auffällt (außer bei der Performance) und sich in Zukunft ändern kann. Beispiele für Namensräume sind: die Menge der eingebauten Namen (die Funktionen wie abs() und eingebaute Ausnahmen enthält), die globalen Namen in einem Modul und die lokalen Namen bei einem Funktionsaufruf. In gewissem Sinne bildet auch die Menge der Attribute eines Objekts einen Namensraum. Das Wichtigste über Namensräume ist, dass es absolut keine Beziehung zwischen Namen in verschiedenen Namensräumen gibt, so können beispielsweise zwei verschiedene Module eine Funktion maximize ohne Verwirrung definieren — Benutzer der Module müssen ihr den Modulnamen voranstellen.
Übrigens verwende ich das Wort Attribut für jeden Namen, der einem Punkt folgt — im Ausdruck z.real ist beispielsweise real ein Attribut des Objekts z. Strenge genommen sind Referenzen auf Namen in Modulen Attributreferenzen: Im Ausdruck modname.funcname ist modname ein Modulobjekt und funcname ein Attribut davon. In diesem Fall gibt es zufällig eine direkte Zuordnung zwischen den Attributen des Moduls und den im Modul definierten globalen Namen: Sie teilen sich denselben Namensraum! [1]
Attribute können schreibgeschützt oder beschreibbar sein. Im letzteren Fall ist eine Zuweisung an Attribute möglich. Modulattribute sind beschreibbar: du kannst modname.the_answer = 42 schreiben. Beschreibbare Attribute „können auch mit der Anweisung del gelöscht werden. Zum Beispiel entfernt del modname.the_answer das Attribut the_answer aus dem Objekt namens modname.
Namensräume werden zu unterschiedlichen Zeitpunkten erstellt und haben unterschiedliche Lebensdauern. Der Namensraum, der die eingebauten Namen enthält, wird beim Start des Python-Interpreters erstellt und niemals gelöscht. Der globale Namensraum für ein Modul wird beim Einlesen der Moduldefinition erstellt, normalerweise bleiben Modulnamensräume ebenfalls bestehen, bis der Interpreter beendet wird. Die Anweisungen, die auf oberster Ebene des Interpreters ausgeführt werden, sei es aus einer Skriptdatei oder interaktiv, gelten als Teil eines Moduls namens __main__ und haben daher ihren eigenen globalen Namensraum. (Die eingebauten Namen befinden sich eigentlich ebenfalls in einem Modul; dieses heißt builtins.)
Der lokale Namensraum für eine Funktion wird beim Aufruf der Funktion erstellt und gelöscht, wenn die Funktion zurückkehrt oder eine Ausnahme auslöst, die nicht innerhalb der Funktion behandelt wird. (Eigentlich wäre Vergessen eine bessere Beschreibung für das, was tatsächlich passiert.) Selbstverständlich haben rekursive Aufrufe jeweils ihren eigenen lokalen Namensraum.
Ein Gültigkeitsbereich (Scope) ist ein Quelltextbereich eines Python-Programms, in dem ein Namensraum direkt zugänglich ist. „Direkt zugänglich“ bedeutet hier, dass eine unqualifizierte Referenz auf einen Namen versucht, den Namen in diesem Namensraum zu finden.
Obwohl Gültigkeitsbereiche statisch bestimmt werden, werden sie dynamisch verwendet. Zu jedem Zeitpunkt der Ausführung gibt es 3 oder 4 verschachtelte Gültigkeitsbereiche, deren Namensräume direkt zugänglich sind:
der innerste Gültigkeitsbereich, der zuerst durchsucht wird, enthält die lokalen Namen
die Gültigkeitsbereiche etwaiger umgebender Funktionen, die ausgehend vom nächstgelegenen umgebenden Bereich durchsucht werden, enthalten nicht-lokale, aber auch nicht-globale Namen
der vorletzte Gültigkeitsbereich enthält die globalen Namen des aktuellen Moduls
der äußerste Gültigkeitsbereich (der zuletzt durchsucht wird) ist der Namensraum, der die eingebauten Namen enthält
Wird ein Name als global deklariert, gehen alle Referenzen und Zuweisungen direkt an den vorletzten Gültigkeitsbereich, der die globalen Namen des Moduls enthält. Um Variablen neuzuweisen, die sich außerhalb des innersten Gültigkeitsbereichs befinden, kann die Anweisung nonlocal verwendet werden, sind sie nicht als nonlocal deklariert, sind diese Variablen schreibgeschützt (ein Versuch, in eine solche Variable zu schreiben, erzeugt lediglich eine neue lokale Variable im innersten Gültigkeitsbereich und lässt die gleichnamige äußere Variable unverändert).
Normally verweist der lokale Gültigkeitsbereich auf die lokalen Namen der (im Quelltext) aktuellen Funktion. Außerhalb von Funktionen verweist der lokale Gültigkeitsbereich auf denselben Namensraum wie der globale Gültigkeitsbereich: den Namensraum des Moduls. Klassendefinitionen platzieren einen weiteren Namensraum im lokalen Gültigkeitsbereich.
Es ist wichtig zu verstehen, dass Gültigkeitsbereiche quelltextbezogen bestimmt werden: Der globale Gültigkeitsbereich einer in einem Modul definierten Funktion ist der Namensraum dieses Moduls, ganz gleich, von wo oder unter welchem Alias die Funktion aufgerufen wird. Andererseits erfolgt die tatsächliche Suche nach Namen dynamisch zur Laufzeit — die Sprachdefinition entwickelt sich jedoch in Richtung einer statischen Namensauflösung zur „Kompilierzeit“, verlasse dich also nicht auf die dynamische Namensauflösung! (Tatsächlich werden lokale Variablen bereits statisch bestimmt.)
Eine Besonderheit von Python ist, dass Zuweisungen an Namen – wenn keine global- oder nonlocal-Anweisung gilt – immer in den innersten Gültigkeitsbereich eingehen. Zuweisungen kopieren keine Daten — sie binden lediglich Namen an Objekte. Das Gleiche gilt für Löschungen: Die Anweisung del x entfernt die Bindung von x aus dem Namensraum, auf den der lokale Gültigkeitsbereich verweist. Tatsächlich nutzen alle Operationen, die neue Namen einführen, den lokalen Gültigkeitsbereich: Insbesondere binden import-Anweisungen und Funktionsdefinitionen den Modul- oder Funktionsnamen im lokalen Gültigkeitsbereich.
Die Anweisung global kann verwendet werden, um anzugeben, dass bestimmte Variablen im globalen Gültigkeitsbereich liegen und dort neu gebunden werden sollen, die Anweisung nonlocal gibt an, dass bestimmte Variablen in einem umgebenden Gültigkeitsbereich liegen und dort neu gebunden werden sollen.
9.2.1. Beispiel für Gültigkeitsbereiche und Namensräume¶
Dies ist ein Beispiel, das zeigt, wie auf die verschiedenen Gültigkeitsbereiche und Namensräume zugegriffen wird und wie global und 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("After local assignment:", spam)
do_nonlocal()
print("After nonlocal assignment:", spam)
do_global()
print("After global assignment:", spam)
scope_test()
print("In global scope:", spam)
Die Ausgabe des Beispielcodes lautet:
After local assignment: test spam
After nonlocal assignment: nonlocal spam
After global assignment: nonlocal spam
In global scope: global spam
Beachte, wie die lokale Zuweisung (die der Standard ist) die Bindung von spam in scope_test nicht verändert hat. Die nonlocal-Zuweisung hat die Bindung von spam in scope_test geändert und die global-Zuweisung hat die Bindung auf Modulebene geändert.
Du kannst auch sehen, dass vor der global-Zuweisung keine vorherige Bindung für spam existierte.
9.3. Ein erster Blick auf Klassen¶
Klassen führen ein wenig neue Syntax, drei neue Objekttypen und einige neue Semantiken ein.
9.3.1. Syntax von Klassendefinitionen¶
Die einfachste Form einer Klassendefinition sieht so aus:
class ClassName:
<statement-1>
.
.
.
<statement-N>
Klassendefinitionen müssen, genau wie Funktionsdefinitionen (def-Anweisungen), ausgeführt werden, bevor sie eine Wirkung haben. (Man könnte eine Klassendefinition denkbarer Weise in einem Zweig einer if-Anweisung oder innerhalb einer Funktion platzieren.)
In der Praxis sind die Anweisungen innerhalb einer Klassendefinition üblicherweise Funktionsdefinitionen, aber andere Anweisungen sind zulässig und manchmal nützlich — darauf kommen wir später zurück. Die Funktionsdefinitionen innerhalb einer Klasse haben normalerweise eine besondere Form von Argumentliste, die durch die Aufrufkonventionen für Methoden vorgegeben ist — auch dies wird später erklärt.
Wird eine Klassendefinition betreten, wird ein neuer Namensraum erzeugt und als lokaler Gültigkeitsbereich verwendet — somit gehen alle Zuweisungen an lokale Variablen in diesen neuen Namensraum. Insbesondere binden Funktionsdefinitionen hier den Namen der neuen Funktion.
Wird eine Klassendefinition normal verlassen (durch das Ende), wird ein Klassenobjekt erzeugt. Dies ist im Wesentlichen eine Hülle um den Inhalt des durch die Klassendefinition erstellten Namensraums; wir werden im nächsten Abschnitt mehr über Klassenobjekte lernen. Der ursprüngliche lokale Gültigkeitsbereich (derjenige, der direkt vor dem Betreten der Klassendefinition galt) wird wiederhergestellt und das Klassenobjekt wird hier an den im Kopf der Klassendefinition angegebenen Klassennamen (ClassName im Beispiel) gebunden.
9.3.2. Klassenobjekte¶
Klassenobjekte unterstützen zwei Arten von Operationen: Attributreferenzen und Instanziierung.
Attributreferenzen verwenden die Standardsyntax, die für alle Attributreferenzen in Python genutzt wird: obj.name. Gültige Attributnamen sind alle Namen, die sich im Namensraum der Klasse befanden, als das Klassenobjekt erzeugt wurde. Wenn die Klassendefinition also so aussah:
class MyClass:
"""A simple example class"""
i = 12345
def f(self):
return 'hello world'
dann sind MyClass.i und MyClass.f gültige Attributreferenzen, die jeweils eine Ganzzahl beziehungsweise ein Funktionsobjekt zurückgeben. Klassenattribute können auch zugewiesen werden, sodass man den Wert von MyClass.i durch eine Zuweisung ändern kann. __doc__ ist ebenfalls ein gültiges Attribut, das den zur Klasse gehörenden Docstring zurückgibt: \"A simple example class\".
Die Instanziierung einer Klasse verwendet Funktionsnotation. Stell dir einfach vor, dass das Klassenobjekt eine parameterlose Funktion ist, die eine neue Instanz der Klasse zurückgibt. Zum Beispiel (unter Annahme der obigen Klasse):
x = MyClass()
erzeugt eine neue Instanz der Klasse und weist dieses Objekt der lokalen Variablen x zu.
Die Instanziierungsoperation (das "Aufrufen" eines Klassenobjekts) erzeugt ein leeres Objekt. Viele Klassen erzeugen Objekte bevorzugt mit Instanzen, die auf einen bestimmten Anfangszustand angepasst sind. Daher kann eine Klasse eine spezielle Methode namens __init__() definieren, wie etwa so:
def __init__(self):
self.data = []
Wenn eine Klasse eine __init__()-Methode definiert, ruft die Klasseninstanziierung automatisch __init__() für die neu erzeugte Klasseninstanz auf. In diesem Beispiel kann eine neue, initialisierte Instanz also wie folgt erzeugt werden:
x = MyClass()
Natürlich kann die __init__()-Methode für mehr Flexibilität auch Argumente besitzen. In diesem Fall werden dem Klasseninstanziierungsoperator übergabene Argumente an __init__() weitergeleitet. 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 nun mit Instanzobjekten tun? Die einzigen Operationen, die von Instanzobjekten verstanden werden, sind Attributreferenzen. Es gibt zwei Arten von gültigen Attributnamen: Datenattribute und Methoden.
data attributes correspond to „instance variables“ in Smalltalk, and to „data
members“ in C++. Data attributes need not be declared; like local variables,
they spring into existence when they are first assigned to. For example, if
x is the instance of MyClass created above, the following piece of
code will print the value 16, without leaving a trace:
x.counter = 1
while x.counter < 10:
x.counter = x.counter * 2
print(x.counter)
del x.counter
Die andere Art von Instanzattributreferenz ist eine Methode. Eine Methode ist eine Funktion, die zu einem Objekt „gehört“.
Gültige Methodennamen eines Instanzobjekts hängen von dessen Klasse ab. Definitionsgemäß definieren alle Attribute einer Klasse, die Funktionsobjekte sind, entsprechende Methoden ihrer Instanzen. In unserem Beispiel ist x.f also eine gültige Methodenreferenz, da MyClass.f eine Funktion ist, x.i hingege nicht, da MyClass.i keine ist. Aber x.f ist nicht dasselbe wie MyClass.f — es ist ein Methodenobjekt, kein Funktionsobjekt.
9.3.4. Methodenobjekte¶
Normalerweise wird eine Methode direkt aufgerufen, nachdem sie gebunden wurde:
x.f()
Wenn x = MyClass() gilt (wie oben), gibt dies die Zeichenkette 'hello world' zurück. Es ist jedoch nicht notwendig, eine Methode sofort aufzurufen: x.f ist ein Methodenobjekt und kann gespeichert und zu einem späteren Zeitpunkt aufgerufen werden. Zum Beispiel:
xf = x.f
while True:
print(xf())
gibt bis ans Ende der Zeit weiterhin hello world aus.
Was genau passiert, wenn eine Methode aufgerufen wird? Du hast vielleicht bemerkt, dass x.f() oben ohne Argument aufgerufen wurde, obwohl die Funktionsdefinition für f() ein Argument angab. Was ist mit dem Argument passiert? Python löst sicherlich eine Ausnahme aus, wenn eine Funktion, die ein Argument erfordert, ohne ein solches aufgerufen wird — selbst wenn das Argument gar nicht verwendet wird…
Eigentlich hast du die Antwort vielleicht schon geahnt: Das Besondere an Methoden ist, dass das Instanzobjekt als erstes Argument der Funktion übergeben wird. In unserem Beispiel ist der Aufruf x.f() genau äquivalent zu MyClass.f(x). Im Allgemeinen entspricht der Aufruf einer Methode mit einer Liste von n Argumenten dem Aufruf der entsprechenden Funktion mit einer Argumentliste, die durch Einfügen des Instanzobjekts der Methode vor dem ersten Argument erstellt wird.
Im Allgemeinen funktionieren Methoden wie folgt: Wenn auf ein Attribut einer Instanz verwiesen wird, das kein Datenattribut ist, wird die Klasse der Instanz durchsucht. Bezeichnet der Name ein gültiges Klassenattribut, das ein Funktionsobjekt ist, werden Referenzen auf das Instanzobjekt und das Funktionsobjekt in ein Methodenobjekt gepackt. Wird das Methodenobjekt mit einer Argumentliste aufgerufen, wird aus dem Instanzobjekt und der Argumentliste eine neue Argumentliste erstellt und das Funktionsobjekt mit dieser neuen Argumentliste aufgerufen.
9.3.5. Klassen- und Instanzvariablen¶
Allgemein gesagt sind Instanzvariablen für Daten gedacht, die für jede Instanz eindeutig sind, während Klassenvariablen für Attribute und Methoden gedacht sind, die von allen Instanzen der Klasse geteilt werden:
class Dog:
kind = 'canine' # class variable shared by all instances
def __init__(self, name):
self.name = name # instance variable unique to each instance
>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.kind # shared by all dogs
'canine'
>>> e.kind # shared by all dogs
'canine'
>>> d.name # unique to d
'Fido'
>>> e.name # unique to e
'Buddy'
Wie in Ein Wort zu Namen und Objekten besprochen, können geteilte Daten in Verbindung mit veränderbaren Objekten wie Listen und Wörterbüchern überraschende Effekte haben. Die Liste tricks im folgenden Code sollte beispielsweise nicht als Klassenvariable verwendet werden, da nur eine einzige Liste von allen Dog-Instanzen geteilt würde:
class Dog:
tricks = [] # falsche Verwendung einer Klassenvariable
def __init__(self, name):
self.name = name
def add_trick(self, trick):
self.tricks.append(trick)
>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks # unerwartet von allen Hunden geteilt
['roll over', 'play dead']
Ein korrektes Design der Klasse sollte stattdessen eine Instanzvariable verwenden:
class Dog:
def __init__(self, name):
self.name = name
self.tricks = [] # erzeugt eine neue leere Liste für jeden Hund
def add_trick(self, trick):
self.tricks.append(trick)
>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks
['roll over']
>>> e.tricks
['play dead']
9.4. Ergänzende Hinweise¶
Tritt derselbe Attributname sowohl in einer Instanz als auch in einer Klasse auf, bevorzugt die Attributsuche die Instanz:
>>> class Warehouse:
... purpose = 'storage'
... region = 'west'
...
>>> w1 = Warehouse()
>>> print(w1.purpose, w1.region)
storage west
>>> w2 = Warehouse()
>>> w2.region = 'east'
>>> print(w2.purpose, w2.region)
storage east
Datenattribute können sowohl von Methoden als auch von gewöhnlichen Benutzern („Clients“) eines Objekts referenziert werden. Mit anderen Worten: Klassen sind nicht dazu geeignet, reine abstrakte Datentypen zu implementieren. In Python gibt es tatsächlich nichts, was eine Kapselung (Data Hiding) erzwingen könnte — alles beruht auf Konventionen. (Andererseits kann die in C geschriebene Python-Implementierung Implementierungsdetails vollständig verbergen und den Zugriff auf ein Objekt bei Bedarf steuern; dies kann von in C geschriebenen Python-Erweiterungen genutzt werden.)
Benutzer sollten Datenattribute mit Vorsicht verwenden — sie können von Methoden aufrechterhaltene Invarianten beschädigen, indem sie deren Datenattribute überschreiben. Beachte, dass Benutzer einem Instanzobjekt eigene Datenattribute hinzufügen können, ohne die Gültigkeit der Methoden zu beeinträchtigen, solange Namenskonflikte vermieden werden — auch hier kann eine Namenskonvention viel Ärger ersparen.
Es gibt keine Kurzschreibweise, um innerhalb von Methoden auf Datenattribute (oder andere Methoden!) zu verweisen. Dies erhöht tatsächlich die Lesbarkeit von Methoden: Beim Durchsehen einer Methode besteht keine Gefahr, lokale Variablen und Instanzvariablen zu verwechseln.
Oft wird das erste Argument einer Methode self genannt. Dies ist nichts weiter als eine Konvention: Der Name self hat für Python absolut keine spezielle Bedeutung. Beachte jedoch, dass dein Code für andere Python-Programmierer schwerer lesbar sein kann, wenn du dich nicht an diese Konvention hältst, und es ist auch denkbar, dass ein Class-Browser-Programm geschrieben wird, das sich auf eine solche Konvention verlässt.
Jedes Funktionsobjekt, das ein Klassenattribut ist, definiert eine Methode für Instanzen dieser Klasse. Es ist nicht erforderlich, dass die Funktionsdefinition textuell in die Klassendefinition eingeschlossen ist: Das Zuweisen eines Funktionsobjekts zu einer lokalen Variable in der Klasse ist ebenfalls in Ordnung. Zum Beispiel:
# Außerhalb der Klasse definierte Funktions
def f1(self, x, y):
return min(x, x+y)
class C:
f = f1
def g(self):
return 'hello world'
h = g
Nun sind f, g und h alles Attribute der Klasse C, die auf Funktionsobjekte verweisen, und folglich sind sie alle Methoden von Instanzen von C — wobei h genau äquivalent zu g ist. Beachte, dass diese Praxis gewöhnlich nur dazu dient, den Leser eines Programms zu verwirren.
Methoden können andere Methoden aufrufen, indem sie Methodenattribute des “
„self-Arguments verwenden:
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 auf globale Namen auf dieselbe Weise verweisen wie gewöhnliche Funktionen. Der mit einer Methode verknüpfte globale Gültigkeitsbereich ist das Modul, das ihre Definition enthält. (Eine Klasse wird nie als globaler Gültigkeitsbereich verwendet.) Während man selten einen guten Grund dafür findet, globale Daten in einer Methode zu verwenden, gibt es viele berechtigte Anwendungen für den globalen Gültigkeitsbereich: Zum einen können in den globalen Gültigkeitsbereich importierte Funktionen und Module von Methoden verwendet werden, ebenso wie darin definierte Funktionen und Klassen. Üblicherweise ist die Klasse, die die Methode enthält, selbst in diesem globalen Gültigkeitsbereich definiert, und im nächsten Abschnitt werden wir einige gute Gründe dafür finden, warum eine Methode auf ihre eigene Klasse verweisen möchte.
Jeder Wert ist ein Objekt und hat daher eine Klasse (auch Typ genannt). Diese ist als object.__class__ gespeichert.
9.5. Vererbung¶
Natürlich wäre ein Sprachfeature des Namens „Klasse“ nicht würdig, wenn es keine Vererbung unterstützen würde. Die Syntax für die Definition einer abgeleiteten Klasse sieht wie folgt aus:
class DerivedClassName(BaseClassName):
<statement-1>
.
.
.
<statement-N>
Der Name BaseClassName muss in einem Namensraum definiert sein, der aus dem Gültigkeitsbereich der abgeleiteten Klassendefinition heraus zugänglich ist. Anstelle eines Basisklassennamens sind auch andere beliebige 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 verläuft genauso wie für eine Basisklasse. Wenn das Klassenobjekt erzeugt wird, wird die Basisklasse gemerkt. Dies wird zur Auflösung von Attributreferenzen verwendet: Wird ein angefordertes Attribut nicht in der Klasse gefunden, wird in der Basisklasse weitergesucht. Diese Regel wird rekursiv angewendet, wenn die Basisklasse selbst von einer anderen Klasse abgeleitet ist.
Die Instanziierung abgeleiteter Klassen weist keine Besonderheiten auf: DerivedClassName() erzeugt eine neue Instanz der Klasse. Methodenreferenzen werden wie folgt aufgelöst: Das entsprechende Klassenattribut wird gesucht, wobei bei Bedarf die Kette der Basisklassen durchsucht wird. Die Methodenreferenz ist gültig, wenn dies ein Funktionsobjekt ergibt.
Abgeleitete Klassen können Methoden ihrer Basisklassen überschreiben. Da Methoden keine besonderen Privilegien beim Aufruf anderer Methoden desselben Objekts besitzen, kann das Aufrufen einer in derselben Basisklasse definierten Methode letztlich dazu führen, dass eine Methode einer abgeleiteten Klasse aufgerufen wird, die diese überschreibt. (Für C++ Programmierer: Alle Methoden in Python sind effektiv virtual.)
Eine überschreibende Methode in einer abgeleiteten Klasse möchte die Methode gleichen Namens der Basisklasse womöglich erweitern, anstatt sie einfach zu ersetzen. Es gibt einen einfachen Weg, die Methode der Basisklasse direkt aufzurufen: Man ruft einfach BaseClassName.methodname(self, argumente) auf. Dies 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 bietet zwei eingebaute Funktionen, die mit Vererbung arbeiten:
Verwende
isinstance(), um den Typ einer Instanz zu überprüfen:isinstance(obj, int)ist nur dannTrue, wennobj.__class__gleichintoder einer vonintabgeleiteten Klasse ist.Verwende
issubclass(), um die Klassenvererbung zu überprüfen:issubclass(bool, int)istTrue, dabooleine Unterklasse vonintist.issubclass(float, int)ist jedochFalse, dafloatkeine Unterklasse vonintist.
9.5.1. Mehrfachvererbung¶
Python unterstützt auch eine Form der Mehrfachvererbung. Eine Klassendefinition mit mehreren Basisklassen sieht wie folgt aus:
class DerivedClassName(Base1, Base2, Base3):
<statement-1>
.
.
.
<statement-N>
Für die meisten Zwecke kann man sich die Suche nach Attributen, die von einer Elternklasse geerbt wurden, in den einfachsten Fällen als Tiefensuche von links nach rechts vorstellen, bei der dieselbe Klasse bei Überschneidungen in der Hierarchie nicht zweimal durchsucht wird. Wenn ein Attribut also nicht in DerivedClassName gefunden wird, wird in Base1 gesucht, dann (rekursiv) in den Basisklassen von Base1, und falls es dort nicht gefunden wurde, wird in Base2 gesucht und so weiter.
Tatsächlich ist es etwas komplexer: Die Method Auflösungsreihenfolge (Method Resolution Order) ändert sich dynamisch, um kooperative Aufrufe von super() zu unterstützen. Dieser Ansatz ist in einigen anderen Sprachen mit Mehrfachvererbung als „call-next-method“ bekannt und ist mächtiger als der Super-Aufruf in Sprachen mit einfacher Vererbung.
Eine dynamische Reihenfolge ist erforderlich, da alle Fälle von Mehrfachvererbung eine oder mehrere Diamant-Beziehungen aufweisen (bei denen auf mindestens eine der Elternklassen über mehrere Pfade von der untersten Klasse aus zugegriffen werden kann). So erben beispielsweise alle Klassen von object, sodass jeder Fall von Mehrfachvererbung mehr als einen Pfad bietet, um object zu erreichen. Um zu verhindern, dass auf die Basisklassen mehr als einmal zugegriffen wird, linearisiert der dynamische Algorithmus die Suchreihenfolge so, dass die in jeder Klasse angegebene Reihenfolge von links nach rechts erhalten bleibt, jede Elternklasse nur einmal aufgerufen wird und die Reihenfolge monoton ist (was bedeutet, dass von einer Klasse abgeleitet werden kann, ohne die Vorrangstellung ihrer Elternklassen zu beeinträchtigen). Zusammengefasst ermöglichen diese Eigenschaften den Entwurf zuverlässiger und erweiterbarer Klassen mit Mehrfachvererbung. Weitere Details finden Sie unter The Python 2.3 Method Resolution Order.
9.6. Private Variablen¶
Private“ Instanzvariablen, auf die nur von innerhalb eines Objekts zugegriffen werden kann, gibt es in Python nicht. Es gibt jedoch eine Konvention, an die sich der meiste Python-Code hält: Ein Name mit einem führenden Unterstrich (z. B. _spam) sollte als nicht-öffentlicher Teil der API behandelt werden (unabhängig davon, ob es sich um eine Funktion, eine Methode oder ein Datenelement handelt). Er sollte als Implementierungsdetail betrachtet werden, das sich ohne Ankündigung ändern kann.
Da es einen berechtigten Anwendungsfall für klassenprivate Elemente gibt (nämlich das Vermeiden von Namenskollisionen mit Namen, die von Unterklassen definiert werden), gibt es eine begrenzte Unterstützung für einen solchen Mechanismus, der als Name-Mangling bezeichnet wird. Jeder Bezeichner der Form __spam (mindestens zwei führende Unterstriche, höchstens ein nachstehender Unterstrich) wird textuell durch _klassenname__spam ersetzt, wobei klassenname der aktuelle Klassenname ohne führende Unterstriche ist. Dieses Umbennenung (Mangling) erfolgt ohne Rücksicht auf die syntaktische Position des Bezeichners, solange es innerhalb der Definition einer Klasse auftritt.
Siehe auch
Die Spezifikationen zur Namensumbenennung privater Elemente für Details und Spezialfälle.
Die Namensumbenennung ist nützlich, um Unterklassen das Überschreiben von Methoden zu ermöglichen, ohne interne Methodenaufrufe innerhalb der Klasse zu beeinträchtigen. 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 Methode update()
class MappingSubclass(Mapping):
def update(self, keys, values):
# provides new signature for update()
# but does not break __init__()
for item in zip(keys, values):
self.items_list.append(item)
Das obige Beispiel würde selbst dann funktionieren, wenn MappingSubclass einen __update-Bezeichner einführen würde, da dieser in der Klasse Mapping durch _Mapping__update bzw. in der Klasse MappingSubclass durch _MappingSubclass__update ersetzt wird.
Beachten Sie, dass die Regeln zur Namensumbenennung hauptsächlich dazu dienen, unbeabsichtigte Kollisionen zu vermeiden. Es ist weiterhin möglich, auf eine Variable zuzugreifen oder diese zu verändern, die als privat gilt. Dies kann unter besonderen Umständen sogar nützlich sein, beispielsweise im Debugger.
Beachten Sie, dass Code, der an exec() oder eval() übergeben wird, den Klassennamen der aufrufenden Klasse nicht als die aktuelle Klasse ansieht. Dies ähnelt der Wirkung der global-Anweisung, deren Wirkung ebenfalls auf Code beschränkt ist, der zusammen byte-kompiliert wird. Die gleiche Einschränkung gilt für getattr(), setattr() und delattr() sowie für die direkte Referenzierung von __dict__.
9.7. Dies und Das¶
Manchmal ist ein Datentyp nützlich, der dem „record“ in Pascal oder der „struct“ in C ähnelt und einige benannte Datenobjekte bündelt. Der idiomatische Ansatz hierfür ist die Verwendung von dataclasses:
from dataclasses import dataclass
@dataclass
class Employee:
name: str
dept: str
salary: int
>>> john = Employee('john', 'computer lab', 1000)
>>> john.dept
'computer lab'
>>> john.salary
1000
Einem Stück Python-Code, das einen bestimmten abstrakten Datentyp erwartet, kann oft stattdessen eine Klasse übergeben werden, die die Methoden dieses Datentyps emuliert. Wenn du beispielsweise eine Funktion hast, die Daten aus einem Dateiobjekt formatiert, kannst du eine Klasse mit den Methoden read() und readline() definieren, die die Daten stattdessen aus einem String-Puffer bezieht, und eine Instanz davon als Argument übergeben.
Instanzmethoden-Objekte besitzen ebenfalls Attribute: m.__self__ ist das Instanzobjekt mit der Methode m(), und m.__func__ ist das der Methode entsprechende Funktionsobjekt.
9.8. Iteratoren¶
Inzwischen hast du wahrscheinlich bemerkt, dass über die meisten Container-Objekte mit einer for-Anweisung iteriert werden kann:
for element in [1, 2, 3]:
print(element)
for element in (1, 2, 3):
print(element)
for key in {'one':1, 'two':2}:
print(key)
for char in "123":
print(char)
for line in open("myfile.txt"):
print(line, end='')
Diese Art des Zugriffs ist klar, prägnant und bequem. Die Verwendung von Iteratoren durchzieht und vereinheitlicht Python. Hinter den Kulissen ruft die for-Anweisung iter() für das Container-Objekt auf. Die Funktion gibt ein Iterator-Objekt zurück, das die Methode __next__() definiert, welche nacheinander auf die Elemente im Container zugreift. Wenn keine Elemente mehr vorhanden sind, löst __next__() eine StopIteration-Ausnahme aus, die der for-Schleife mitteilt, dass sie beendet werden soll. Du kannst die Methode __next__() über die eingebaute Funktion next() aufrufen. Dieses Beispiel zeigt, wie das Ganze funktioniert::“
>>> s = 'abc'
>>> it = iter(s)
>>> it
<str_iterator object at 0x10c90e650>
>>> next(it)
'a'
>>> next(it)
'b'
>>> next(it)
'c'
>>> next(it)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
next(it)
StopIteration
Nachdem du die Mechanik hinter dem Iterator-Protokoll gesehen hast, ist es einfach, deinen Klassen Iterator-Verhalten hinzuzufügen. Definiere eine __iter__()-Methode, die ein Objekt mit einer __next__()-Methode zurückgibt. Wenn die Klasse __next__() definiert, kann __iter__() einfach self zurückgeben:
class Reverse:
"""Iterator zum Durchlaufen einer Sequenz in umgekehrter Reihenfolge."""
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.9. Generatoren¶
Generatoren sind ein einfaches und mächtiges Werkzeug zum Erstellen von Iteratoren. Sie werden wie gewöhnliche Funktionen geschrieben, verwenden aber die yield-Anweisung, wann immer sie Daten zurückgeben wollen. Jedes Mal, wenn next() darauf aufgerufen wird, setzt der Generator an der Stelle fort, an der er aufgehört hat (er merkt sich alle Datenwerte und welche Anweisung zuletzt ausgeführt wurde). Ein Beispiel zeigt, dass Generatoren denkbar einfach zu erstellen sind:
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 gemacht werden kann, lässt sich auch mit klassenbasierten Iteratoren umsetzen, wie im vorherigen Abschnitt beschrieben. Was Generatoren so kompakt macht, ist, dass die Methoden __iter__() und __next__() automatisch erstellt werden.
Ein weiteres Schlüsselelement ist, dass die lokalen Variablen und der Ausführungszustand zwischen den Aufrufen automatisch gespeichert werden. Dadurch war die Funktion einfacher zu schreiben und viel übersichtlicher als ein Ansatz mit Instanzvariablen wie self.index und self.data.
Neben der automatischen Methodenerstellung und dem Speichern des Programmzustands lösen Generatoren beim Beenden automatisch StopIteration aus. In Kombination machen es diese Funktionen einfach, Iteratoren mit nicht mehr Aufwand als beim Schreiben einer normalen Funktion zu erstellen.
9.10. Generator-Ausdrücke¶
Einige einfache Generatoren können prägnant als Ausdrücke geschrieben werden, wobei eine Syntax ähnlich der von List-Comprehensions verwendet wird, jedoch mit runden statt eckigen Klammern. Diese Ausdrücke sind für Situationen konzipiert, in denen der Generator direkt von einer umgebenden Funktion verwendet wird. Generator-Ausdrücke sind kompakter, aber weniger vielseitig als vollständige Generator-Definitionen und tendenziell speicherschonender als entsprechende List-Comprehensions.
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
>>> 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