9. Klassen¶
Klassen bieten eine Möglichkeit, Daten und Funktionalität zu bündeln. Die Erstellung einer neuen Klasse erzeugt einen neuen *Typ* von Objekt, wodurch neue *Instanzen* dieses Typs erstellt werden können. Jede Klasseninstanz kann Attribute zur Zustandsverwaltung haben. Klasseninstanzen können auch Methoden (definiert durch ihre Klasse) zur Zustandsänderung haben.
Im Vergleich zu anderen Programmiersprachen fügt Pythons Klassensystem Klassen mit einem Minimum an neuer Syntax und Semantik hinzu. Es ist eine Mischung aus den Klassensystemen, die in C++ und Modula-3 zu finden sind. Python-Klassen bieten alle Standardfunktionen der objektorientierten Programmierung: Der Klassenvererbungsmechanismus ermöglicht Mehrfachvererbung von Basisklassen, eine abgeleitete Klasse kann jede Methode ihrer Basisklasse(n) überschreiben, und eine Methode kann die Methode einer Basisklasse mit demselben Namen aufrufen. Objekte können beliebige Mengen und Arten von Daten enthalten. Wie bei Modulen partizipieren Klassen an der dynamischen Natur von Python: Sie werden zur Laufzeit erstellt und können nach der Erstellung weiter modifiziert werden.
In C++-Terminologie sind normalerweise Klassenmember (einschließlich der Datenmember) *öffentlich* (siehe jedoch unten Private Variablen), und alle Memberfunktionen sind *virtuell*. Wie in Modula-3 gibt es keine Kurzschriften, um auf die Member eines Objekts aus seinen Methoden zuzugreifen: Die Methodenfunktion wird mit einem expliziten ersten Argument deklariert, das das Objekt repräsentiert und implizit durch den Aufruf bereitgestellt wird. Wie in Smalltalk sind Klassen selbst Objekte. Dies ermöglicht Semantik für Import und Umbenennung. Im Gegensatz zu C++ und Modula-3 können eingebaute Typen als Basisklassen für die Erweiterung durch den Benutzer verwendet werden. Außerdem können, wie in C++, die meisten integrierten Operatoren mit spezieller Syntax (arithmetische Operatoren, Indizierung usw.) für Klasseninstanzen neu definiert werden.
(Mangels universell akzeptierter Terminologie zur Klassendiskussion werde ich gelegentlich Begriffe aus Smalltalk und C++ verwenden. Ich würde Begriffe aus Modula-3 verwenden, da dessen objektorientierte Semantik näher an der von Python liegt als die von C++, aber ich gehe davon aus, dass nur 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. Dies ist in anderen Sprachen als Aliasing bekannt. Dies wird normalerweise beim ersten Blick auf Python nicht geschätzt und kann bei der Arbeit mit unveränderlichen Basistypen (Zahlen, Zeichenketten, Tupel) sicher ignoriert werden. Aliasing hat jedoch einen möglicherweise überraschenden Effekt auf die Semantik von Python-Code, der veränderliche Objekte wie Listen, Wörterbücher und die meisten anderen Typen betrifft. Dies wird normalerweise zum Vorteil des Programms genutzt, da Aliase in gewisser Hinsicht wie Zeiger funktionieren. Zum Beispiel ist das Übergeben eines Objekts billig, da die Implementierung nur einen Zeiger übergibt; und wenn eine Funktion ein als Argument übergebenes Objekt modifiziert, sieht der Aufrufer die Änderung – dies eliminiert die Notwendigkeit zweier unterschiedlicher Argumentübergabemechanismen wie in Pascal.
9.2. Python-Gültigkeitsbereiche und Namensräume¶
Bevor ich Klassen einführe, muss ich Ihnen etwas über Pythons Gültigkeitsbereichsregeln erzählen. Klassendefinitionen machen einige nette Tricks mit Namensräumen, und Sie müssen wissen, wie Gültigkeitsbereiche und Namensräume funktionieren, um vollständig zu verstehen, was vor sich geht. Übrigens ist Wissen über dieses Thema für jeden fortgeschrittenen Python-Programmierer nützlich.
Beginnen wir mit einigen Definitionen.
Ein *Namensraum* ist eine Abbildung von Namen auf Objekte. Die meisten Namensräume sind derzeit als Python-Wörterbücher implementiert, aber das ist normalerweise in keiner Weise bemerkbar (außer bei der Leistung) und kann sich in Zukunft ändern. Beispiele für Namensräume sind: die Menge der eingebauten Namen (die Funktionen wie abs() und eingebaute Ausnahmeramen enthält); die globalen Namen in einem Modul; und die lokalen Namen in einem Funktionsaufruf. In gewissem Sinne bilden die Attribute eines Objekts ebenfalls einen Namensraum. Wichtig ist zu wissen, dass es absolut keine Beziehung zwischen Namen in verschiedenen Namensräumen gibt; zum Beispiel können zwei verschiedene Module beide eine Funktion maximize definieren, ohne Verwirrung zu stiften – Benutzer der Module müssen sie mit dem Modulnamen präfixieren.
Nebenbei bemerkt, verwende ich das Wort *Attribut* für jeden Namen, der auf einen Punkt folgt – zum Beispiel im Ausdruck z.real ist real ein Attribut des Objekts z. Streng genommen sind Referenzen auf Namen in Modulen Attributreferenzen: Im Ausdruck modname.funcname ist modname ein Modulobjekt und funcname ist ein Attribut davon. In diesem Fall gibt es zufällig eine direkte Abbildung zwischen den Attributen des Moduls und den globalen Namen, die im Modul definiert sind: Sie teilen sich denselben Namensraum! [1]
Attribute können schreibgeschützt oder beschreibbar sein. Im letzteren Fall ist die Zuweisung zu Attributen möglich. Modulattribute sind beschreibbar: Sie können modname.the_answer = 42 schreiben. Beschreibbare Attribute können auch mit der del-Anweisung gelöscht werden. Zum Beispiel wird del modname.the_answer das Attribut the_answer aus dem durch modname benannten Objekt entfernen.
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 nie gelöscht. Der globale Namensraum für ein Modul wird erstellt, wenn die Moduldefinition eingelesen wird; normalerweise bleiben Modulnamensräume bis zum Beenden des Interpreters erhalten. Die vom Top-Level-Aufruf des Interpreters ausgeführten Anweisungen, entweder aus einer Skriptdatei gelesen oder interaktiv, werden als Teil eines Moduls namens __main__ betrachtet, sodass sie ihren eigenen globalen Namensraum haben. (Die eingebauten Namen leben eigentlich auch in einem Modul; dieses heißt builtins.)
Der lokale Namensraum für eine Funktion wird erstellt, wenn die Funktion aufgerufen wird, und gelöscht, wenn die Funktion zurückkehrt oder eine Ausnahme auslöst, die nicht innerhalb der Funktion behandelt wird. (Tatsächlich wäre "vergessen" ein besserer Weg, um zu beschreiben, was tatsächlich passiert.) Natürlich haben rekursive Aufrufe jeweils ihren eigenen lokalen Namensraum.
Ein *Gültigkeitsbereich* ist ein textueller Bereich eines Python-Programms, in dem ein Namensraum direkt zugänglich ist. "Direkt zugänglich" bedeutet hier, dass ein nicht qualifizierter Name versucht, den Namen im Namensraum zu finden.
Obwohl Gültigkeitsbereiche statisch bestimmt werden, werden sie dynamisch verwendet. Zu jedem Zeitpunkt während 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 von beliebigen umschließenden Funktionen, die beginnend mit dem nächstgelegenen umschließenden Gültigkeitsbereich durchsucht werden, enthalten nicht-globale, aber auch nicht-lokale Namen
der vorletzte Gültigkeitsbereich enthält die globalen Namen des aktuellen Moduls
der äußerste Gültigkeitsbereich (zuletzt durchsucht) ist der Namensraum, der eingebaute Namen enthält
Wenn ein Name als global deklariert wird, gehen alle Referenzen und Zuweisungen direkt in den vorletzten Gültigkeitsbereich, der die globalen Namen des Moduls enthält. Um Variablen außerhalb des innersten Gültigkeitsbereichs neu zu binden, kann die nonlocal-Anweisung verwendet werden; wenn sie nicht als nonlocal deklariert ist, sind diese Variablen schreibgeschützt (ein Versuch, eine solche Variable zu schreiben, erstellt einfach eine *neue* lokale Variable im innersten Gültigkeitsbereich und lässt die gleichnamige äußere Variable unverändert).
Normalerweise bezieht sich der lokale Gültigkeitsbereich auf die lokalen Namen der (textuell) aktuellen Funktion. Außerhalb von Funktionen bezieht sich der lokale Gültigkeitsbereich auf denselben Namensraum wie der globale Gültigkeitsbereich: den Namensraum des Moduls. Klassendefinitionen platzieren noch einen weiteren Namensraum im lokalen Gültigkeitsbereich.
Es ist wichtig zu erkennen, dass Gültigkeitsbereiche textuell bestimmt werden: Der globale Gültigkeitsbereich einer Funktion, die in einem Modul definiert ist, ist der Namensraum dieses Moduls, unabhängig davon, von wo oder unter welchem Alias die Funktion aufgerufen wird. Andererseits erfolgt die eigentliche Suche nach Namen dynamisch, zur Laufzeit – die Sprachdefinition entwickelt sich jedoch hin zu statischer Namensauflösung zur "Kompilierungszeit", verlassen Sie sich also nicht auf dynamische Namensauflösung! (Tatsächlich werden lokale Variablen bereits statisch bestimmt.)
Eine Besonderheit von Python ist, dass – wenn keine global- oder nonlocal-Anweisung aktiv ist – Zuweisungen an Namen immer in den innersten Gültigkeitsbereich gehen. Zuweisungen kopieren keine Daten – sie binden nur Namen an Objekte. Dasselbe gilt für Löschungen: Die Anweisung del x entfernt die Bindung von x aus dem durch den lokalen Gültigkeitsbereich referenzierten Namensraum. Tatsächlich führen alle Operationen, die neue Namen einführen, den lokalen Gültigkeitsbereich aus: Insbesondere binden import-Anweisungen und Funktionsdefinitionen den Modul- oder Funktionsnamen im lokalen Gültigkeitsbereich.
Die global-Anweisung kann verwendet werden, um anzugeben, dass bestimmte Variablen im globalen Gültigkeitsbereich leben und dort neu gebunden werden sollten; die nonlocal-Anweisung gibt an, dass bestimmte Variablen in einem umschließenden Gültigkeitsbereich leben und dort neu gebunden werden sollten.
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 ist
After local assignment: test spam
After nonlocal assignment: nonlocal spam
After global assignment: nonlocal spam
In global scope: global spam
Beachten Sie, wie die *lokale* Zuweisung (die Standardeinstellung) die Bindung von *spam* in *scope_test* nicht geä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.
Sie können auch sehen, dass vor der global-Zuweisung keine vorherige Bindung für *spam* vorhanden war.
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 der Klassendefinition¶
Die einfachste Form einer Klassendefinition sieht so aus:
class ClassName:
<statement-1>
.
.
.
<statement-N>
Klassendefinitionen müssen, wie Funktionsdefinitionen (def-Anweisungen), ausgeführt werden, bevor sie wirksam werden. (Sie könnten eine Klassendefinition konzeptionell in einen Zweig einer if-Anweisung oder in eine Funktion setzen.)
In der Praxis sind die Anweisungen innerhalb einer Klassendefinition normalerweise Funktionsdefinitionen, aber andere Anweisungen sind erlaubt und manchmal nützlich – darauf werden wir später zurückkommen. Die Funktionsdefinitionen innerhalb einer Klasse haben normalerweise eine besondere Form der Argumentliste, die durch die Aufrufkonventionen für Methoden bestimmt wird – auch dies wird später erklärt.
Wenn eine Klassendefinition betreten wird, wird ein neuer Namensraum erstellt 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.
Wenn eine Klassendefinition normal verlassen wird (am Ende), wird ein *Klassenobjekt* erstellt. Dies ist im Grunde ein Wrapper um den Inhalt des durch die Klassendefinition erstellten Namensraums; mehr über Klassenobjekte erfahren wir im nächsten Abschnitt. Der ursprüngliche lokale Gültigkeitsbereich (derjenige, der kurz vor dem Betreten der Klassendefinition aktiv war) wird wiederhergestellt, und das Klassenobjekt wird hier an den Klassennamen gebunden, der in der Kopfzeile der Klassendefinition angegeben ist (ClassName im Beispiel).
9.3.2. Klassenobjekte¶
Klassenobjekte unterstützen zwei Arten von Operationen: Attributreferenzen und Instanziierung.
*Attributreferenzen* verwenden die Standard-Syntax, die für alle Attributreferenzen in Python verwendet wird: obj.name. Gültige Attributnamen sind alle Namen, die sich zum Zeitpunkt der Erstellung des Klassenobjekts im Namensraum der Klasse befanden. 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 eine Ganzzahl bzw. ein Funktions-Objekt zurückgeben. Klassenattribute können auch zugewiesen werden, sodass Sie den Wert von MyClass.i durch Zuweisung ändern können. __doc__ ist ebenfalls ein gültiges Attribut und gibt den Docstring der Klasse zurück: "Eine einfache Beispielklasse".
Die *Instanziierung* von Klassen erfolgt im Funktionsstil. Tun Sie einfach so, als wäre das Klassenobjekt eine parameterlose Funktion, die eine neue Instanz der Klasse zurückgibt. Zum Beispiel (unter der Annahme der obigen Klasse)
x = MyClass()
erstellt eine neue *Instanz* der Klasse und weist dieses Objekt der lokalen Variablen x zu.
Die Instanziierungsoperation ("Aufruf" eines Klassenobjekts) erstellt ein leeres Objekt. Viele Klassen möchten Objekte mit Instanzen erstellen, die auf einen bestimmten Anfangszustand angepasst sind. Daher kann eine Klasse eine spezielle Methode namens __init__() definieren, wie folgt:
def __init__(self):
self.data = []
Wenn eine Klasse eine __init__()-Methode definiert, ruft die Klasseninstanziierung automatisch __init__() für die neu erstellte Klasseninstanz auf. In diesem Beispiel kann also eine neue, initialisierte Instanz erhalten werden durch:
x = MyClass()
Natürlich kann die __init__()-Methode Argumente für größere Flexibilität haben. In diesem Fall werden Argumente, die dem Klasseninstanziierungsoperator übergeben werden, an __init__() weitergegeben. 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 Instanzobjekten machen? Die einzigen Operationen, die von Instanzobjekten verstanden werden, sind Attributreferenzen. Es gibt zwei Arten von gültigen Attributnamen: Datenattribute und Methoden.
*Datenattribute* entsprechen "Instanzvariablen" in Smalltalk und "Datenmitgliedern" in C++. Datenattribute müssen nicht deklariert werden; wie lokale Variablen entstehen sie, wenn ihnen zum ersten Mal ein Wert zugewiesen wird. Wenn x beispielsweise die oben erstellte Instanz von MyClass ist, gibt der folgende Codeausschnitt den Wert 16 aus, ohne eine Spur zu hinterlassen:
x.counter = 1
while x.counter < 10:
x.counter = x.counter * 2
print(x.counter)
del x.counter
Die andere Art der Instanzattributreferenz ist eine *Methode*. Eine Methode ist eine Funktion, die zu einem Objekt "gehört".
Gültige Methodennamen eines Instanzobjekts hängen von seiner Klasse ab. Per Definition definieren alle Attribute einer Klasse, die Funktions-Objekte sind, entsprechende Methoden für ihre Instanzen. In unserem Beispiel ist x.f eine gültige Methodenreferenz, da MyClass.f eine Funktion ist, aber x.i nicht, da MyClass.i keine Funktion ist. Aber x.f ist nicht dasselbe wie MyClass.f – es ist ein *Methoden-Objekt*, kein Funktions-Objekt.
9.3.4. Methodenobjekte¶
Normalerweise wird eine Methode sofort aufgerufen, nachdem sie gebunden wurde:
x.f()
Wenn x = MyClass(), wie oben, gibt dies die Zeichenkette 'hallo welt' zurück. Es ist jedoch nicht notwendig, eine Methode sofort aufzurufen: x.f ist ein Methoden-Objekt und kann zur Seite gelegt und später aufgerufen werden. Zum Beispiel:
xf = x.f
while True:
print(xf())
wird weiterhin "hallo welt" bis zum Ende der Zeit ausgeben.
Was genau passiert, wenn eine Methode aufgerufen wird? Sie haben vielleicht bemerkt, dass x.f() oben ohne Argument aufgerufen wurde, obwohl die Funktionsdefinition für f() ein Argument spezifiziert hat. Was ist mit dem Argument passiert? Sicherlich löst Python eine Ausnahme aus, wenn eine Funktion, die ein Argument benötigt, ohne eines aufgerufen wird – selbst wenn das Argument nicht tatsächlich verwendet wird...
Tatsächlich haben Sie die Antwort vielleicht erraten: Das Besondere an Methoden ist, dass das Instanzobjekt als erstes Argument der Funktion übergeben wird. In unserem Beispiel ist der Aufruf x.f() exakt äquivalent zu MyClass.f(x). Im Allgemeinen ist der Aufruf einer Methode mit einer Liste von *n* Argumenten äquivalent zum Aufruf der entsprechenden Funktion mit einer Argumentliste, die durch Einfügen des Instanzobjekts der Methode vor das erste Argument erstellt wird.
Im Allgemeinen funktionieren Methoden wie folgt. Wenn auf ein Nicht-Datenattribut einer Instanz zugegriffen wird, wird die Klasse der Instanz durchsucht. Wenn der Name ein gültiges Klassenattribut ist, das ein Funktions-Objekt ist, werden Referenzen auf sowohl das Instanzobjekt als auch das Funktions-Objekt in ein Methoden-Objekt gepackt. Wenn das Methoden-Objekt mit einer Argumentliste aufgerufen wird, wird eine neue Argumentliste aus dem Instanzobjekt und der Argumentliste erstellt, und das Funktions-Objekt wird mit dieser neuen Argumentliste aufgerufen.
9.3.5. Klassen- und Instanzvariablen¶
Im Allgemeinen sind Instanzvariablen für Daten gedacht, die für jede Instanz eindeutig sind, und Klassenvariablen für Attribute und Methoden, die von allen Instanzen der Klasse gemeinsam genutzt 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 diskutiert, kann die gemeinsame Datennutzung möglicherweise überraschende Auswirkungen auf die Beteiligung an veränderlichen Objekten wie Listen und Wörterbüchern haben. Zum Beispiel sollte die *tricks*-Liste im folgenden Code nicht als Klassenvariable verwendet werden, da nur eine einzige Liste von allen *Dog*-Instanzen gemeinsam genutzt würde:
class Dog:
tricks = [] # mistaken use of a class variable
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 # unexpectedly shared by all dogs
['roll over', 'play dead']
Die korrekte Gestaltung der Klasse sollte stattdessen eine Instanzvariable verwenden:
class Dog:
def __init__(self, name):
self.name = name
self.tricks = [] # creates a new empty list for each dog
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. Zufällige Anmerkungen¶
Wenn derselbe Attributname sowohl in einer Instanz als auch in einer Klasse vorkommt, priorisiert 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 von Methoden sowie von normalen Benutzern ("Clients") eines Objekts referenziert werden. Mit anderen Worten, Klassen können nicht zur Implementierung reiner abstrakter Datentypen verwendet werden. Tatsächlich ist nichts in Python möglich, um Datenverdeckung zu erzwingen – alles basiert auf Konvention. (Andererseits kann die Python-Implementierung, die in C geschrieben ist, Implementierungsdetails vollständig verbergen und den Zugriff auf ein Objekt bei Bedarf steuern; dies kann von Erweiterungen für Python genutzt werden, die in C geschrieben sind.)
Clients sollten Datenattribute mit Vorsicht verwenden – Clients können durch Überschreiben von Datenattributen die von den Methoden aufrechterhaltenen Invarianten durcheinanderbringen. Beachten Sie, dass Clients eigene Datenattribute zu einem Instanzobjekt hinzufügen können, ohne die Gültigkeit der Methoden zu beeinträchtigen, solange Namenskonflikte vermieden werden – auch hier kann eine Namenskonvention viele Kopfschmerzen ersparen.
Es gibt keine Kurzschrift, um Datenattribute (oder andere Methoden!) aus Methoden heraus zu referenzieren. Ich finde, das erhöht tatsächlich die Lesbarkeit von Methoden: Es besteht keine Gefahr, lokale und Instanzvariablen beim flüchtigen Durchsehen einer Methode 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 keinerlei besondere Bedeutung. Beachten Sie jedoch, dass Sie durch Nichtbefolgung der Konvention Ihren Code für andere Python-Programmierer weniger lesbar machen und dass es auch denkbar ist, dass ein *Klassen-Browser*-Programm geschrieben wird, das auf einer solchen Konvention basiert.
Jedes Funktions-Objekt, das ein Klassenattribut ist, definiert eine Methode für Instanzen dieser Klasse. Es ist nicht notwendig, dass die Funktionsdefinition textuell in der Klassendefinition enthalten ist: Die Zuweisung eines Funktions-Objekts an eine lokale Variable in der Klasse ist ebenfalls in Ordnung. Zum Beispiel:
# Function defined outside the class
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 alle Attribute der Klasse C, die auf Funktions-Objekte verweisen, und folglich sind sie alle Methoden von Instanzen von C – wobei h exakt äquivalent zu g ist. Beachten Sie, dass diese Praxis normalerweise 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 globale Namen auf die gleiche Weise wie normale Funktionen referenzieren. Der globale Gültigkeitsbereich, der mit einer Methode verbunden ist, ist das Modul, das ihre Definition enthält. (Eine Klasse wird nie als globaler Gültigkeitsbereich verwendet.) Während man selten einen guten Grund für die Verwendung globaler Daten in einer Methode findet, gibt es viele legitime Verwendungen des globalen Gültigkeitsbereichs: Zum einen können in den globalen Gültigkeitsbereich importierte Funktionen und Module von Methoden verwendet werden, ebenso wie dort definierte Funktionen und Klassen. Normalerweise wird die Klasse, die die Methode enthält, selbst in diesem globalen Gültigkeitsbereich definiert, und im nächsten Abschnitt finden wir einige gute Gründe, warum eine Methode auf ihre eigene Klasse verweisen möchte.
Jeder Wert ist ein Objekt und hat daher eine *Klasse* (auch sein *Typ* genannt). Sie ist als object.__class__ gespeichert.
9.5. Vererbung¶
Natürlich wäre eine Sprachfunktion ohne Unterstützung für Vererbung den Namen "Klasse" nicht wert. Die Syntax für eine abgeleitete Klassendefinition sieht so aus:
class DerivedClassName(BaseClassName):
<statement-1>
.
.
.
<statement-N>
Der Name BaseClassName muss in einem Namensraum definiert sein, der vom Gültigkeitsbereich, der die abgeleitete Klassendefinition enthält, zugänglich ist. Anstelle eines Basisklassennamens sind auch beliebige andere Ausdrücke zulässig. 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 bei einer Basisklasse. Wenn das Klassenobjekt konstruiert wird, wird die Basisklasse gespeichert. Dies wird zur Auflösung von Attributreferenzen verwendet: Wenn ein angefordertes Attribut nicht in der Klasse gefunden wird, wird die Suche fortgesetzt, um in der Basisklasse nachzuschauen. Diese Regel wird rekursiv angewendet, wenn die Basisklasse selbst von einer anderen Klasse abgeleitet ist.
Bei der Instanziierung abgeleiteter Klassen gibt es nichts Besonderes: DerivedClassName() erstellt eine neue Instanz der Klasse. Methodenreferenzen werden wie folgt aufgelöst: Das entsprechende Klassenattribut wird durchsucht, wobei bei Bedarf die Kette der Basisklassen hinuntergegangen wird, und die Methodenreferenz ist gültig, wenn dies ein Funktions-Objekt ergibt.
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 aufruft, die in derselben Basisklasse definiert ist, dazu führen, dass eine Methode einer abgeleiteten Klasse aufgerufen wird, die sie überschreibt. (Für C++-Programmierer: Alle Methoden in Python sind effektiv *virtuell*.)
Eine überschreibende Methode in einer abgeleiteten Klasse möchte möglicherweise die Basisklassenmethode mit demselben Namen erweitern, anstatt sie einfach zu ersetzen. Es gibt eine einfache Möglichkeit, die Basisklassenmethode direkt aufzurufen: Rufen Sie einfach BaseClassName.methodname(self, arguments) auf. Dies ist gelegentlich auch für Clients nützlich. (Beachten Sie, dass dies nur funktioniert, wenn die Basisklasse unter dem Namen BaseClassName im globalen Gültigkeitsbereich zugänglich ist.)
Python hat zwei eingebaute Funktionen, die mit Vererbung arbeiten:
Verwenden Sie
isinstance(), um den Typ einer Instanz zu überprüfen:isinstance(obj, int)ist nur dannTrue, wennobj.__class__intoder eine vonintabgeleitete Klasse ist.Verwenden Sie
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 so aus:
class DerivedClassName(Base1, Base2, Base3):
<statement-1>
.
.
.
<statement-N>
Für die meisten Zwecke kann man in den einfachsten Fällen die Suche nach Attributen, die von einer Elternklasse geerbt wurden, als Tiefensuche (depth-first), von links nach rechts betrachten, ohne in derselben Klasse zweimal zu suchen, wo es Überschneidungen in der Hierarchie gibt. Wenn also ein Attribut nicht in DerivedClassName gefunden wird, wird es in Base1 gesucht, dann (rekursiv) in den Basisklassen von Base1, und wenn es dort nicht gefunden wurde, wurde es in Base2 gesucht, und so weiter.
Tatsächlich ist es etwas komplexer; die Methode zur Auflösung der Reihenfolge ändert sich dynamisch, um kooperative Aufrufe an super() zu unterstützen. Dieser Ansatz ist in einigen anderen Mehrfachvererbungssprachen als call-next-method bekannt und leistungsfähiger als der Super-Aufruf, der in Single-Inheritance-Sprachen zu finden ist.
Eine dynamische Reihenfolge ist notwendig, da alle Fälle von Mehrfachvererbung eine oder mehrere Diamant-Beziehungen aufweisen (bei denen mindestens eine der Elternklassen über mehrere Pfade von der untersten Klasse aus erreichbar ist). Zum Beispiel erben alle Klassen von object, sodass jeder Fall von Mehrfachvererbung mehr als einen Pfad bietet, um object zu erreichen. Um zu verhindern, dass die Basisklassen mehr als einmal aufgerufen werden, linearisiert der dynamische Algorithmus die Suchreihenfolge auf eine Weise, die die links-nach-rechts-Reihenfolge beibehält, die in jeder Klasse angegeben ist, jeden Elternteil nur einmal aufruft und monoton ist (was bedeutet, dass eine Klasse untervererbt werden kann, ohne die Vorrangreihenfolge ihrer Eltern zu beeinträchtigen). Zusammengenommen machen diese Eigenschaften es möglich, zuverlässige und erweiterbare Klassen mit Mehrfachvererbung zu entwerfen. Weitere Details finden Sie unter The Python 2.3 Method Resolution Order.
9.6. Private Variablen¶
„Private“ Instanzvariablen, auf die nicht von außerhalb eines Objekts zugegriffen werden kann, existieren in Python nicht. Es gibt jedoch eine Konvention, die von den meisten Python-Codes befolgt wird: Ein Name, dem ein Unterstrich vorangestellt ist (z. B. _spam), sollte als nicht-öffentlicher Teil der API behandelt werden (egal ob es sich um eine Funktion, eine Methode oder ein Datenelement handelt). Er sollte als Implementierungsdetail betrachtet und kann ohne Vorankündigung geändert werden.
Da es einen gültigen Anwendungsfall für klasseninterne Member gibt (nämlich Namenskonflikte mit Namen zu vermeiden, die von Unterklassen definiert wurden), gibt es eine eingeschränkte Unterstützung für einen solchen Mechanismus, der als Name Mangling bezeichnet wird. Jede Kennung der Form __spam (mindestens zwei führende Unterstriche, höchstens ein nachgestellter Unterstrich) wird textuell durch _klassenname__spam ersetzt, wobei klassenname der aktuelle Klassenname mit gestrippten führenden Unterstrichen ist. Dieses Mangling erfolgt unabhängig von der syntaktischen Position der Kennung, solange sie innerhalb der Definition einer Klasse auftritt.
Siehe auch
Weitere Details und Sonderfälle finden Sie in den Spezifikationen für das private Namens-Mangling.
Namens-Mangling ist hilfreich, damit Unterklassen Methoden überschreiben können, ohne die Aufrufe von Methoden 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 copy of original update() method
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 auch dann funktionieren, wenn MappingSubclass eine __update Kennung einführen würde, da sie in der Mapping Klasse zu _Mapping__update und in der MappingSubclass Klasse zu _MappingSubclass__update wird.
Beachten Sie, dass die Mangling-Regeln hauptsächlich dazu dienen, unbeabsichtigte Zugriffe zu vermeiden; es ist immer noch möglich, auf eine Variable zuzugreifen oder sie zu ändern, die als privat betrachtet wird. Dies kann sogar in besonderen Umständen nützlich sein, z. B. im Debugger.
Beachten Sie, dass Code, der an exec() oder eval() übergeben wird, den Klassennamen der aufrufenden Klasse nicht als aktuelle Klasse betrachtet; dies ähnelt der Wirkung der global-Anweisung, deren Wirkung ebenfalls auf Code beschränkt ist, der gemeinsam kompiliert wird. Die gleiche Einschränkung gilt für getattr(), setattr() und delattr(), sowie bei der direkten Referenzierung von __dict__.
9.7. Sonstiges¶
Manchmal ist es nützlich, einen Datentyp ähnlich dem Pascal „record“ oder C „struct“ zu haben, der einige benannte Datenelemente bündelt. Der idiomatische Ansatz ist die Verwendung von dataclasses für diesen Zweck.
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
Ein Stück Python-Code, das einen bestimmten abstrakten Datentyp erwartet, kann oft eine Klasse übergeben bekommen, die die Methoden dieses Datentyps emuliert. Wenn Sie beispielsweise eine Funktion haben, die Daten aus einem Dateiobjekt formatiert, können Sie eine Klasse mit den Methoden read() und readline() definieren, die die Daten aus einem String-Puffer abrufen, und diese als Argument übergeben.
Instanzmethoden-Objekte haben ebenfalls Attribute: Instanzmethoden-Objekte haben ebenfalls Attribute: m.__self__ ist das Instanzobjekt mit der Methode m(), und m.__func__ ist das Funktionsobjekt, das der Methode entspricht.
9.8. Iteratoren¶
Bis jetzt haben Sie wahrscheinlich bemerkt, dass die meisten Container-Objekte mit einer for-Schleife durchlaufen werden können
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='')
Dieser Zugriffsstil ist klar, prägnant und praktisch. Die Verwendung von Iteratoren durchdringt und vereinheitlicht Python. Im Hintergrund ruft die for-Schleife iter() für das Container-Objekt auf. Die Funktion gibt ein Iterator-Objekt zurück, das die Methode __next__() definiert, die 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. Sie können die Methode __next__() mit der integrierten Funktion next() aufrufen; dieses Beispiel zeigt, wie alles 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 wir die Mechanik hinter dem Iterator-Protokoll gesehen haben, ist es einfach, Iterator-Verhalten zu Ihren Klassen hinzuzufügen. Definieren Sie eine Methode __iter__(), die ein Objekt mit einer Methode __next__() zurückgibt. Wenn die Klasse __next__() definiert, 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.9. Generatoren¶
Generatoren sind ein einfaches und leistungsfähiges Werkzeug zum Erstellen von Iteratoren. Sie werden wie normale Funktionen geschrieben, verwenden aber die yield-Anweisung, wann immer sie Daten zurückgeben möchten. Jedes Mal, wenn next() aufgerufen wird, wird der Generator dort fortgesetzt, wo er aufgehört hat (er speichert alle Datenwerte und die zuletzt ausgeführte Anweisung). Ein Beispiel zeigt, dass Generatoren sehr 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, kann auch mit klassenbasierten Iteratoren wie im vorherigen Abschnitt beschrieben gemacht werden. Was Generatoren so kompakt macht, ist, dass die Methoden __iter__() und __next__() automatisch erstellt werden.
Ein weiteres wichtiges Merkmal ist, dass die lokalen Variablen und der Ausführungszustand automatisch zwischen den Aufrufen gespeichert werden. Dies machte die Funktion einfacher zu schreiben und viel klarer als ein Ansatz mit Instanzvariablen wie self.index und self.data.
Zusätzlich zur automatischen Methodenerstellung und dem Speichern des Programmzustands lösen Generatoren beim Beenden automatisch StopIteration aus. In Kombination machen diese Funktionen es einfach, Iteratoren ohne mehr Aufwand als das Schreiben einer regulären Funktion zu erstellen.
9.10. Generator-Ausdrücke¶
Einige einfache Generatoren können prägnant als Ausdrücke mit einer Syntax ähnlich wie List Comprehensions, aber mit Klammern anstelle von eckigen Klammern codiert werden. Diese Ausdrücke sind für Situationen gedacht, in denen der Generator sofort von einer umschließenden Funktion verwendet wird. Generator-Ausdrücke sind kompakter, aber weniger vielseitig als vollständige Generator-Definitionen und tendieren dazu, speichereffizienter zu sein als äquivalente List Comprehensions.
Beispiele
>>> sum(i*i for i in range(10)) # sum of squares
285
>>> xvec = [10, 20, 30]
>>> yvec = [7, 5, 3]
>>> sum(x*y for x,y in zip(xvec, yvec)) # dot product
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