Design und Geschichte FAQ¶
Warum verwendet Python Einrückung, um Anweisungen zu gruppieren?¶
Guido van Rossum ist der Ansicht, dass Einrückung zur Gruppierung ausgesprochen elegant ist und viel zur Klarheit des durchschnittlichen Python-Programms beiträgt. Die meisten Leute lernen diese Eigenschaft nach einer Weile zu lieben.
Da es keine Klammern für Anfang und Ende gibt, kann es keine Abweichung zwischen der Gruppierung geben, die der Parser sieht, und der, die der menschliche Leser sieht. Gelegentlich stoßen C-Programmierer auf ein Codefragment wie dieses:
if (x <= y)
x++;
y--;
z++;
Nur die Anweisung x++ wird ausgeführt, wenn die Bedingung wahr ist, aber die Einrückung lässt viele etwas anderes glauben. Selbst erfahrene C-Programmierer starren das manchmal lange an und fragen sich, warum y auch für x > y verringert wird.
Weil es keine Klammern für Anfang und Ende gibt, ist Python viel weniger anfällig für Streit über den Programmierstil. In C gibt es viele verschiedene Arten, die geschweiften Klammern zu setzen. Hat man sich daran gewöhnt, Code in einem bestimmten Stil zu lesen und zu schreiben, ist es normal, sich beim Lesen eines anderen Stils etwas unbehaglich zu fühlen — oder wenn man ihn schreiben muss.
Viele Programmierstile setzen die Klammern für Anfang und Ende auf eigene Zeilen. Das macht Programme deutlich länger und verschwendet wertvollen Platz auf dem Bildschirm, wodurch es schwerer wird, ein Programm im Überblick zu erfassen. Im Idealfall sollte eine Funktion auf einen Bildschirm passen, sagen wir 20–30 Zeilen. 20 Zeilen Python können viel mehr leisten als 20 Zeilen C. Das liegt nicht allein an den fehlenden Klammern für Anfang und Ende – die fehlenden Deklarationen und die höheren Datentypen tragen ebenfalls dazu bei –, aber die auf Einrückung beruhende Syntax hilft durchaus.
Warum bekomme ich bei einfachen Rechenoperationen seltsame Ergebnisse?¶
Nächste Frage ansehen.
Warum sind Gleitkommaberechnungen so ungenau?¶
Nutzer sind oft von solechen Ergebnissen überrascht:
>>> 1.2 - 1.0
0.19999999999999996
und halten es für einen Fehler in Python. Ist es aber nicht. Das hat wenig mit Python zu tun und viel mehr damit, wie die darunter liegende Plattform mit Gleitkommazahlen umgeht.
Der Typ float in CPython verwendet zur Speicherung ein double aus C. Der Wert eines float-Objekts wird als binäre Gleitkommazahl mit fester Genauigkeit gespeichert, typischerweise 53 Bit, und Python verwendet für Gleitkommaoperationen C-Operationen, die sich ihrerseits auf die Umsetzung in der Hardware des Prozessors stützen. Das bedeutet, dass sich Python bei Gleitkommaoperationen wie viele verbreitete Sprachen verhält, darunter C und Java.
Viele Zahlen, die sich in Dezimalschreibweise leicht hinschreiben lassen, sind als binäre Gleitkommazahl nicht genau darstellbar. Zum Beispiel ist nach:
>>> x = 1.2
der für x gespeicherte Wert eine (sehr gute) Annäherung an den Dezimalwert 1.2, aber nicht genau gleich. Auf einer typischen Maschine ist der tatsächlich gespeicherte Wert:
1.0011001100110011001100110011001100110011001100110011 (binary)
was genau dies ist:
1.1999999999999999555910790149937383830547332763671875 (decimal)
Die typische Genauigkeit von 53 Bit gibt Python-Gleitkommazahlen eine Genauigkeit von 15–16 Dezimalstellen.
Eine ausführlichere Erklärung findest du im Kapitel Gleitkommaarithmetik im Python-Tutorial.
Warum sind Zeichenketten in Python unveränderlich?¶
Es gibt mehrere Vorteile.
Ein Grund ist die Geschwindigkeit: Zu wissen, dass eine Zeichenkette unveränderlich ist, bedeutet, dass wir den Platz für sie bei ihrer Erzeugung reservieren können, und der Speicherbedarf liegt fest und ändert sich nicht. Das ist auch einer der Gründe für die Unterscheidung zwischen Tupeln und Listen.
Ein weiterer Vorteil ist, dass Zeichenketten in Python als genauso „elementar“ gelten wie Zahlen. Keine Aktivität der Welt ändert den Wert 8 in etwas anderes, und in Python ändert keine Aktivität der Welt die Zeichenkette „eight“ in etwas anderes.
Warum muss ‚self‘ in Methodendefinitionen und -aufrufen ausdrücklich angegeben werden?¶
Die Idee wurde von Modula-3 übernommen. Sie erweist sich aus verschiedenen Gründen als sehr nützlich.
Erstens ist offensichtlicher, dass du eine Methode oder ein Instanzattribut verwendest und nicht eine lokale Variable. self.x oder self.meth() zu lesen macht völlig klar, dass eine Instanzvariable oder Methode verwendet wird, selbst wenn du die Klassendefinition nicht im Kopf hast. In C++ kannst du es einigermaßen daran erkennen, dass eine Deklaration der lokalen Variablen fehlt – vorausgesetzt, globale Variablen sind selten oder leicht erkennbar. In Python gibt es aber keine Deklarationen lokaler Variablen, du müsstest also in der Klassendefinition nachsehen, um sicher zu sein. Manche Programmierrichtlinien für C++ und Java verlangen für Instanzattribute das Präfix m_, diese Ausdrücklichkeit ist also auch in jenen Sprachen nützlich.
Zweitens bedeutet es, dass keine besondere Syntax nötig ist, wenn du die Methode einer bestimmten Klasse ausdrücklich ansprechen oder aufrufen willst. In C++ musst du den Operator :: verwenden, wenn du eine Methode einer Basisklasse nutzen willst, die in einer abgeleiteten Klasse überschrieben ist – in Python schreibst du baseclass.methodname(self, <argument list>). Das ist besonders nützlich für __init__()-Methoden und allgemein in Fällen, in denen die Methode einer abgeleiteten Klasse die gleichnamige Methode der Basisklasse erweitern und sie deshalb irgendwie aufrufen will.
Drittens löst es bei Instanzvariablen ein syntaktisches Problem mit der Zuweisung. Da lokale Variablen in Python per Definition jene Variablen sind, denen im Rumpf einer Funktion ein Wert zugewiesen wird und die nicht ausdrücklich als global deklariert sind, muss es eine Möglichkeit geben, dem Interpreter zu sagen, dass eine Zuweisung einer Instanzvariablen und nicht einer lokalen Variablen gelten soll — und das sollte aus Effizienzgründen besser syntaktisch geschehen. C++ macht das über Deklarationen, aber Python hat keine Deklarationen, und es wäre schade, sie nur zu diesem Zweck einzuführen. Das ausdrückliche self.var löst das elegant. Entsprechend bedeutet es beim Verwenden von Instanzvariablen, dass Verweise auf unqualifizierte Namen innerhalb einer Methode nicht die Dictionarys der Instanz durchsuchen müssen. Anders gesagt: Lokale Variablen und Instanzvariablen leben in zwei verschiedenen Namensräumen, und du musst Python sagen, welcher gemeint ist.
Warum kann ich in einem Ausdruck keine Zuweisung verwenden?¶
Ab Python 3.8 kannst du das!
Zuweisungsausdrücke mit dem Walross-Operator := weisen einer Variablen innerhalb eines Ausdrucks einen Wert zu:
while chunk := fp.read(200):
print(chunk)
Weitere Informationen stehen in PEP 572.
Warum verwendet Python für manches Methoden (z. B. list.index()), für anderes aber Funktionen (z. B. len(list))?¶
Guido sagte dazu:
(a) For some operations, prefix notation just reads better than postfix – prefix (and infix!) operations have a long tradition in mathematics which likes notations where the visuals help the mathematician thinking about a problem. Compare the easy with which we rewrite a formula like x*(a+b) into x*a + x*b to the clumsiness of doing the same thing using a raw OO notation.
(b) When I read code that says len(x) I know that it is asking for the length of something. This tells me two things: the result is an integer, and the argument is some kind of container. To the contrary, when I read x.len(), I have to already know that x is some kind of container implementing an interface or inheriting from a class that has a standard len(). Witness the confusion we occasionally have when a class that is not implementing a mapping has a get() or keys() method, or something that isn’t a file has a write() method.
—https://mail.python.org/pipermail/python-3000/2006-November/004643.html
Warum ist join() eine Methode von Zeichenketten und nicht von Listen oder Tupeln?¶
Zeichenketten wurden ab Python 1.6 den anderen Standardtypen viel ähnlicher, als Methoden hinzukamen, die dieselbe Funktionalität bieten wie die schon immer verfügbaren Funktionen des Moduls string. Die meisten dieser neuen Methoden sind weithin akzeptiert, aber die eine, bei der sich manche Programmierer offenbar unbehaglich fühlen, ist:
", ".join(['1', '2', '4', '8', '16'])
was dieses Ergebnis liefert:
"1, 2, 4, 8, 16"
Gegen diese Verwendung gibt es zwei verbreitete Einwände.
Der erste geht in diese Richtung: „Es sieht wirklich hässlich aus, eine Methode eines Zeichenkettenliterals (einer Zeichenkettenkonstante) zu verwenden.“ Die Antwort darauf ist, dass es das vielleicht tut, ein Zeichenkettenliteral aber einfach ein fester Wert ist. Wenn die Methoden auf Namen erlaubt sein sollen, die an Zeichenketten gebunden sind, gibt es keinen logischen Grund, sie auf Literalen zu verbieten.
Der zweite Einwand lautet typischerweise: „Ich sage doch eigentlich einer Sequenz, sie solle ihre Elemente mit einer Zeichenkettenkonstante verbinden.“ Leider nicht. Aus irgendeinem Grund scheint es deutlich weniger Schwierigkeiten damit zu geben, dass split() eine Methode von Zeichenketten ist, denn dort sieht man leicht, dass
"1, 2, 4, 8, 16".split(", ")
eine Anweisung an ein Zeichenkettenliteral ist, die Teilzeichenketten zurückzugeben, die durch das angegebene Trennzeichen begrenzt sind (oder, voreingestellt, durch beliebige Folgen von Leerraum).
join() ist eine Methode von Zeichenketten, weil du damit der Trennzeichenkette sagst, sie solle über eine Folge von Zeichenketten iterieren und sich selbst zwischen benachbarte Elemente einfügen. Diese Methode lässt sich mit jedem Argument verwenden, das den Regeln für Sequenzobjekte folgt, auch mit neuen Klassen, die du selbst festlegst. Entsprechende Methoden gibt es für bytes- und bytearray-Objekte.
Wie schnell sind Ausnahmen?¶
Ein try/except-Block ist ausgesprochen effizient, solange keine Ausnahmen ausgelöst werden. Eine Ausnahme tatsächlich abzufangen ist teuer. In Python-Versionen vor 2.0 war diese Schreibweise üblich:
try:
value = mydict[key]
except KeyError:
mydict[key] = getvalue(key)
value = mydict[key]
Das war nur dann sinnvoll, wenn du erwartet hast, dass das Dictionary den Schlüssel fast immer enthält. War das nicht der Fall, hast du es so geschrieben:
if key in mydict:
value = mydict[key]
else:
value = mydict[key] = getvalue(key)
Für diesen speziellen Fall könntest du auch value = dict.setdefault(key, getvalue(key)) verwenden, aber nur, wenn der Aufruf von getvalue() günstig genug ist, denn er wird in jedem Fall ausgewertet.
Warum gibt es in Python keine switch- oder case-Anweisung?¶
Allgemein führen strukturierte switch-Anweisungen einen Codeblock aus, wenn ein Ausdruck einen bestimmten Wert oder eine bestimmte Menge von Werten hat. Seit Python 3.10 lassen sich literale Werte oder Konstanten innerhalb eines Namensraums bequem mit einer match ... case-Anweisung abgleichen. Weitere Informationen zu match-Anweisungen stehen in der Spezifikation und im Tutorial. Eine ältere Alternative ist eine Folge von if... elif... elif... else.
Für Fälle, in denen du aus sehr vielen Möglichkeiten auswählen musst, kannst du ein Dictionary anlegen, das die Fallwerte auf die aufzurufenden Funktionen abbildet. Zum Beispiel:
functions = {'a': function_1,
'b': function_2,
'c': self.method_1}
func = functions[value]
func()
Um Methoden auf Objekten aufzurufen, kannst du es noch weiter vereinfachen, indem du die integrierte Funktion getattr() verwendest, um Methoden mit einem bestimmten Namen zu holen:
class MyVisitor:
def visit_a(self):
...
def dispatch(self, value):
method_name = 'visit_' + str(value)
method = getattr(self, method_name)
method()
Es wird empfohlen, für die Methodennamen ein Präfix zu verwenden, in diesem Beispiel visit_. Ohne so ein Präfix könnte ein Angreifer jede Methode deines Objekts aufrufen, falls die Werte aus einer nicht vertrauenswürdigen Quelle kommen.
switch mit Durchfallen nachzubilden, wie beim switch-case-default aus C, ist möglich, aber deutlich schwieriger und wird seltener gebraucht.
Könnt ihr Threads nicht im Interpreter nachbilden, statt sich auf eine betriebssystemspezifische Thread-Umsetzung zu stützen?¶
Antwort 1: Leider legt der Interpreter für jeden Python-Stapelrahmen mindestens einen C-Stapelrahmen an. Außerdem können Erweiterungen zu fast beliebigen Zeitpunkten in Python zurückrufen. Eine vollständige Thread-Umsetzung erfordert deshalb Thread-Unterstützung für C.
Antwort 2: Zum Glück gibt es Stackless Python mit einer völlig neu entworfenen Interpreterschleife, die den C-Stapel umgeht.
Warum dürfen Lambda-Ausdrücke keine Anweisungen enthalten?¶
Lambda-Ausdrücke in Python dürfen keine Anweisungen enthalten, weil das syntaktische Gerüst von Python keine Anweisungen verarbeiten kann, die in Ausdrücken verschachtelt sind. In Python ist das aber kein ernstes Problem. Anders als Lambda-Formen in anderen Sprachen, wo sie Funktionalität hinzufügen, sind Lambdas in Python nur eine Kurzschreibweise für den Fall, dass man zu faul ist, eine Funktion zu definieren.
Funktionen sind in Python bereits Objekte erster Klasse und lassen sich in einem lokalen Gültigkeitsbereich deklarieren. Der einzige Vorteil eines Lambdas gegenüber einer lokal definierten Funktion besteht deshalb darin, dass du für die Funktion keinen Namen erfinden musst – aber das ist nur eine lokale Variable, der das Funktionsobjekt zugewiesen wird, und zwar genau derselbe Objekttyp, den ein Lambda-Ausdruck liefert!
Lässt sich Python zu Maschinencode, C oder einer anderen Sprache kompilieren?¶
Cython übersetzt eine abgewandelte Fassung von Python mit optionalen Annotationen in C-Erweiterungen. Nuitka ist ein aufstrebender Compiler, der Python in C++-Code übersetzt und die vollständige Sprache unterstützen will.
Wie verwaltet Python den Speicher?¶
Die Einzelheiten der Speicherverwaltung von Python hängen von der Implementierung ab. Die Standardimplementierung CPython verwendet Referenzzählung, um unerreichbare Objekte zu erkennen, und einen weiteren Mechanismus, um Referenzzyklen einzusammeln: Sie führt regelmäßig einen Algorithmus zur Zykluserkennung aus, der nach unerreichbaren Zyklen sucht und die beteiligten Objekte löscht. Das Modul gc stellt Funktionen bereit, um eine Speicherbereinigung durchzuführen, Statistiken zur Fehlersuche abzurufen und die Parameter der Bereinigung einzustellen.
Andere Implementierungen wie Jython oder PyPy können sich dagegen auf einen anderen Mechanismus stützen, etwa auf eine vollwertige Speicherbereinigung. Dieser Unterschied kann zu feinen Problemen beim Portieren führen, wenn dein Python-Code vom Verhalten der Referenzzählung abhängt.
In manchen Python-Implementierungen gehen dem folgenden Code vermutlich die Dateideskriptoren aus, obwohl er unter CPython in Ordnung ist:
for file in very_long_list_of_files:
f = open(file)
c = f.read(1)
Mit der Referenzzählung und dem Destruktorschema von CPython schließt nämlich jede neue Zuweisung an f die vorherige Datei. Mit einer herkömmlichen Speicherbereinigung werden diese Dateiobjekte dagegen erst in unterschiedlichen und möglicherweise langen Abständen eingesammelt und geschlossen.
Willst du Code schreiben, der mit jeder Python-Implementierung funktioniert, solltest du die Datei ausdrücklich schließen oder die with-Anweisung verwenden. Das funktioniert unabhängig vom Schema der Speicherverwaltung:
for file in very_long_list_of_files:
with open(file) as f:
c = f.read(1)
Warum verwendet CPython kein herkömmlicheres Schema zur Speicherbereinigung?¶
Zum einen ist das keine Standardeigenschaft von C und damit nicht portabel. (Ja, wir kennen die Boehm-GC-Bibliothek. Sie hat Teile in Assembler für die meisten verbreiteten Plattformen, aber nicht für alle, und obwohl sie größtenteils transparent ist, ist sie es nicht vollständig. Es sind Patches nötig, damit Python mit ihr funktioniert.)
Eine herkömmliche Speicherbereinigung wird außerdem zum Problem, wenn Python in andere Anwendungen eingebettet wird. Während es in einem eigenständigen Python in Ordnung ist, das übliche malloc() und free() durch Fassungen aus der GC-Bibliothek zu ersetzen, möchte eine Anwendung, die Python einbettet, vielleicht ihren eigenen Ersatz für malloc() und free() haben und nicht den von Python. Derzeit arbeitet CPython mit allem zusammen, was malloc() und free() sauber umsetzt.
Warum wird beim Beenden von CPython nicht aller Speicher freigegeben?¶
Objekte, auf die aus den globalen Namensräumen von Python-Modulen verwiesen wird, werden beim Beenden von Python nicht immer freigegeben. Das kann passieren, wenn es zirkuläre Verweise gibt. Außerdem gibt es bestimmte Speicherbereiche, die von der C-Bibliothek belegt werden und sich unmöglich freigeben lassen (ein Werkzeug wie Purify beschwert sich darüber). Python geht beim Aufräumen des Speichers am Ende aber entschlossen vor und versucht durchaus, jedes einzelne Objekt zu zerstören.
Willst du Python dazu zwingen, bestimmte Dinge bei der Freigabe zu löschen, verwende das Modul atexit, um eine Funktion auszuführen, die diese Löschungen erzwingt.
Warum gibt es getrennte Datentypen für Tupel und Listen?¶
Listen und Tupel sind sich in vielem ähnlich, werden aber im Allgemeinen auf grundlegend verschiedene Weise verwendet. Tupel lassen sich als ähnlich zu records in Pascal oder structs in C auffassen. Sie sind kleine Sammlungen zusammengehöriger Daten, die von unterschiedlichem Typ sein können und als Gruppe bearbeitet werden. Eine kartesische Koordinate wird zum Beispiel passend als Tupel aus zwei oder drei Zahlen dargestellt.
Listen sind dagegen eher wie Arrays in anderen Sprachen. Sie enthalten in der Regel eine wechselnde Anzahl von Objekten, die alle denselben Typ haben und die einzeln bearbeitet werden. So gibt os.listdir('.') zum Beispiel eine Liste von Zeichenketten zurück, die die Dateien im aktuellen Verzeichnis benennen. Funktionen, die mit dieser Ausgabe arbeiten, gingen normalerweise nicht kaputt, wenn du dem Verzeichnis eine oder zwei weitere Dateien hinzufügst.
Tupel sind unveränderlich, das heißt, sobald ein Tupel erzeugt ist, kannst du keines seiner Elemente durch einen neuen Wert ersetzen. Listen sind veränderlich, das heißt, du kannst die Elemente einer Liste jederzeit ändern. Als Schlüssel eines Dictionarys lassen sich nur hashbare Objekte verwenden. Die meisten unveränderlichen Typen sind hashbar, und deshalb lassen sich Tupel als Schlüssel verwenden, Listen aber nicht. Beachte aber, dass ein Tupel nur dann hashbar ist, wenn alle seine Elemente hashbar sind.
Wie sind Listen in CPython umgesetzt?¶
Die Listen von CPython sind in Wirklichkeit Arrays veränderlicher Länge und keine verketteten Listen im Stil von Lisp. Die Umsetzung verwendet ein zusammenhängendes Array von Referenzen auf andere Objekte und hält in einer Kopfstruktur der Liste einen Zeiger auf dieses Array sowie dessen Länge.
Dadurch ist der Zugriff auf ein Listenelement a[i] eine Operation, deren Kosten unabhängig von der Größe der Liste und vom Wert des Index sind.
Werden Elemente angehängt oder eingefügt, wird die Größe des Arrays von Referenzen angepasst. Dabei kommt einiger Scharfsinn zum Einsatz, um das wiederholte Anhängen von Elementen zu beschleunigen: Wenn das Array wachsen muss, wird etwas zusätzlicher Platz belegt, sodass die nächsten Male keine tatsächliche Größenänderung nötig ist.
Die Kosten der verschiedenen Listenoperationen stehen unter Zeitkomplexität von Operationen auf eingebauten Typen.
Wie sind Dictionarys in CPython umgesetzt?¶
Die Dictionarys von CPython sind als Hashtabellen veränderlicher Größe umgesetzt. Gegenüber B-Bäumen bringt das unter den meisten Umständen eine bessere Geschwindigkeit beim Nachschlagen — der mit Abstand häufigsten Operation —, und die Umsetzung ist einfacher.
Dictionarys arbeiten so, dass sie mit der integrierten Funktion hash() für jeden im Dictionary gespeicherten Schlüssel einen Hashwert berechnen. Der Hashwert unterscheidet sich stark je nach Schlüssel und je nach einem prozessweiten Startwert. So könnte 'Python' zu -539294296 hashen, während 'python', eine Zeichenkette, die sich nur um ein einziges Bit unterscheidet, zu 1142331976 hashen könnte. Der Hashwert wird dann verwendet, um eine Position in einem internen Array zu berechnen, an der der Wert gespeichert wird. Angenommen, du speicherst Schlüssel, die alle unterschiedliche Hashwerte haben, dann bedeutet das, dass Dictionarys zum Abrufen eines Schlüssels konstante Zeit brauchen – O(1) in der Groß-O-Schreibweise.
Die Kosten der verschiedenen Dictionary-Operationen stehen unter Zeitkomplexität von Operationen auf eingebauten Typen.
Warum müssen Schlüssel eines Dictionarys unveränderlich sein?¶
Die Umsetzung von Dictionarys als Hashtabelle verwendet einen Hashwert, der aus dem Wert des Schlüssels berechnet wird, um den Schlüssel zu finden. Wäre der Schlüssel ein veränderliches Objekt, könnte sich sein Wert ändern und damit auch sein Hashwert. Da aber derjenige, der das Schlüsselobjekt ändert, nicht erkennen kann, dass es als Schlüssel eines Dictionarys verwendet wird, kann er den Eintrag im Dictionary nicht verschieben. Wenn du dann dasselbe Objekt im Dictionary nachschlägst, wird es nicht gefunden, weil sein Hashwert ein anderer ist. Würdest du den alten Wert nachschlagen, würde er ebenfalls nicht gefunden, weil der Wert des Objekts in diesem Hash-Behälter ein anderer wäre.
Willst du ein Dictionary mit einer Liste als Index, wandle die Liste einfach zuerst in ein Tupel um. Die Funktion tuple(L) erzeugt ein Tupel mit denselben Einträgen wie die Liste L. Tupel sind unveränderlich und lassen sich deshalb als Schlüssel eines Dictionarys verwenden.
Einige vorgeschlagene, aber untaugliche Lösungen:
Listen über ihre Adresse hashen, also über die Objekt-ID. Das funktioniert nicht, denn wenn du eine neue Liste mit demselben Wert aufbaust, wird sie nicht gefunden, zum Beispiel:
mydict = {[1, 2]: '12'} print(mydict[[1, 2]])
würde eine
KeyError-Ausnahme auslösen, weil die ID des in der zweiten Zeile verwendeten[1, 2]sich von der in der ersten Zeile unterscheidet. Anders gesagt sollten Schlüssel eines Dictionarys mit==verglichen werden und nicht mitis.Eine Kopie anlegen, wenn eine Liste als Schlüssel verwendet wird. Das funktioniert nicht, weil die Liste als veränderliches Objekt einen Verweis auf sich selbst enthalten könnte und der kopierende Code dann in eine Endlosschleife liefe.
Listen als Schlüssel erlauben, dem Benutzer aber sagen, er solle sie nicht verändern. Das würde eine ganze Klasse schwer auffindbarer Fehler in Programmen zulassen, nämlich wenn du es vergisst oder eine Liste versehentlich änderst. Es macht außerdem eine wichtige Invariante von Dictionarys zunichte: Jeder Wert in
d.keys()ist als Schlüssel des Dictionarys verwendbar.Listen als nur lesbar kennzeichnen, sobald sie als Schlüssel eines Dictionarys verwendet werden. Das Problem ist, dass nicht nur das Objekt der obersten Ebene seinen Wert ändern könnte: Du könntest ein Tupel, das eine Liste enthält, als Schlüssel verwenden. Irgendetwas als Schlüssel in ein Dictionary einzutragen würde bedeuten, alle von dort aus erreichbaren Objekte als nur lesbar zu kennzeichnen – und auch hier könnten selbstbezügliche Objekte eine Endlosschleife verursachen.
Es gibt einen Trick, das zu umgehen, falls du ihn brauchst, aber auf eigene Gefahr: Du kannst eine veränderliche Struktur in eine Klasseninstanz hüllen, die sowohl eine Methode __eq__() als auch __hash__() hat. Dann musst du sicherstellen, dass der Hashwert aller solchen Hüllobjekte, die in einem Dictionary liegen (oder in einer anderen auf Hashwerten beruhenden Struktur), unverändert bleibt, solange das Objekt im Dictionary (oder in der anderen Struktur) liegt.
class ListWrapper:
def __init__(self, the_list):
self.the_list = the_list
def __eq__(self, other):
return self.the_list == other.the_list
def __hash__(self):
l = self.the_list
result = 98767 - len(l)*555
for i, el in enumerate(l):
try:
result = result + (hash(el) % 9999999) * 1001 + i
except Exception:
result = (result % 7777777) + i * 333
return result
Beachte, dass die Hashberechnung dadurch verkompliziert wird, dass einige Elemente der Liste nicht hashbar sein könnten und dass es zu einem arithmetischen Überlauf kommen könnte.
Darüber hinaus muss immer gelten: Wenn o1 == o2 gilt (d. h. o1.__eq__(o2) is True), dann auch hash(o1) == hash(o2) (d. h. o1.__hash__() == o2.__hash__()), unabhängig davon, ob das Objekt in einem Dictionary liegt oder nicht. Hältst du diese Einschränkungen nicht ein, verhalten sich Dictionarys und andere auf Hashwerten beruhende Strukturen fehlerhaft.
Im Fall von ListWrapper darf sich die eingehüllte Liste nicht ändern, solange das Hüllobjekt in einem Dictionary liegt, sonst kommt es zu Unstimmigkeiten. Mach das nur, wenn du bereit bist, gründlich über die Anforderungen und über die Folgen nachzudenken, sie nicht richtig einzuhalten. Du bist gewarnt.
Warum gibt list.sort() nicht die sortierte Liste zurück?¶
In Situationen, in denen die Geschwindigkeit zählt, wäre es Verschwendung, eine Kopie der Liste nur zum Sortieren anzulegen. Deshalb sortiert list.sort() die Liste an ihrem Platz. Um dich daran zu erinnern, gibt die Methode die sortierte Liste nicht zurück. So lässt du dich nicht dazu verleiten, versehentlich eine Liste zu überschreiben, wenn du eine sortierte Kopie brauchst, die unsortierte Fassung aber behalten willst.
Willst du eine neue Liste zurückbekommen, verwende stattdessen die integrierte Funktion sorted(). Diese Funktion erzeugt aus einem übergebenen iterierbaren Objekt eine neue Liste, sortiert sie und gibt sie zurück. So durchläufst du zum Beispiel die Schlüssel eines Dictionarys in sortierter Reihenfolge:
for key in sorted(mydict):
... # mach irgendwas mit mydict[key] ...
Wie gibt man in Python eine Schnittstellenspezifikation an und setzt sie durch?¶
Eine Schnittstellenspezifikation für ein Modul, wie Sprachen etwa C++ und Java sie bereitstellen, beschreibt die Prototypen der Methoden und Funktionen des Moduls. Viele finden, dass es beim Bau großer Programme hilft, wenn Schnittstellenspezifikationen zur Übersetzungszeit durchgesetzt werden.
Python 2.6 fügt ein Modul abc hinzu, mit dem du abstrakte Basisklassen (ABCs) festlegen kannst. Mit isinstance() und issubclass() kannst du dann prüfen, ob eine Instanz oder eine Klasse eine bestimmte ABC umsetzt. Das Modul collections.abc legt eine Reihe nützlicher ABCs fest, etwa Iterable, Container und MutableMapping.
Für Python lassen sich viele der Vorteile von Schnittstellenspezifikationen durch eine passende Testdisziplin für die Bestandteile erreichen.
Eine gute Testsammlung für ein Modul kann gleichzeitig als Regressionstest dienen und als Schnittstellenspezifikation des Moduls samt einer Reihe von Beispielen. Viele Python-Module lassen sich als Skript ausführen und bieten dann einen einfachen „Selbsttest“. Selbst Module, die komplexe externe Schnittstellen verwenden, lassen sich oft isoliert testen, indem man die externe Schnittstelle mit einfachen Platzhaltern nachbildet. Mit den Modulen doctest und unittest oder mit Testframeworks von Dritten lassen sich erschöpfende Testsammlungen bauen, die jede Codezeile eines Moduls durchlaufen.
Eine passende Testdisziplin kann beim Bau großer, komplexer Anwendungen in Python genauso helfen wie Schnittstellenspezifikationen. Sie kann sogar besser sein, denn eine Schnittstellenspezifikation kann bestimmte Eigenschaften eines Programms nicht prüfen. So wird von der Methode list.append() erwartet, dass sie neue Elemente am Ende einer internen Liste hinzufügt. Eine Schnittstellenspezifikation kann nicht prüfen, ob deine Umsetzung von list.append() das tatsächlich richtig macht, in einer Testsammlung ist diese Eigenschaft aber mühelos zu prüfen.
Testsammlungen zu schreiben ist sehr hilfreich, und du solltest deinen Code vielleicht so entwerfen, dass er leicht zu testen ist. Eine immer beliebtere Technik, die testgetriebene Entwicklung, verlangt, Teile der Testsammlung zuerst zu schreiben, bevor du irgendwelchen eigentlichen Code schreibst. Natürlich erlaubt Python dir auch, schludrig zu sein und überhaupt keine Testfälle zu schreiben.
Warum gibt es kein goto?¶
In den 1970er Jahren erkannte man, dass unbeschränktes goto zu unordentlichem „Spaghetti“-Code führen kann, der schwer zu verstehen und zu überarbeiten ist. In einer höheren Programmiersprache ist es außerdem unnötig, solange es Möglichkeiten gibt zu verzweigen (in Python mit if-Anweisungen und den Ausdrücken or, and und if/else) und zu schleifen (mit while- und for-Anweisungen, die continue und break enthalten können).
Man kann Ausnahmen auch verwenden, um ein „strukturiertes goto“ bereitzustellen, das sogar über Funktionsaufrufe hinweg funktioniert. Viele finden, dass Ausnahmen alle vernünftigen Verwendungen der Konstrukte go oder goto aus C, Fortran und anderen Sprachen bequem nachbilden können. Zum Beispiel:
class label(Exception): pass # ein Label deklarieren
try:
...
if condition: raise label() # goto label
...
except label: # wohin das goto führt
pass
...
Damit kannst du nicht in die Mitte einer Schleife springen, aber das gilt ohnehin meist als Missbrauch von goto. Sparsam verwenden.
Warum dürfen rohe Zeichenketten (r-Strings) nicht mit einem Rückwärtsschrägstrich enden?¶
Genauer gesagt dürfen sie nicht mit einer ungeraden Anzahl von Rückwärtsschrägstrichen enden: Der ungepaarte Rückwärtsschrägstrich am Ende maskiert das schließende Anführungszeichen, wodurch eine unbeendete Zeichenkette übrig bleibt.
Rohe Zeichenketten wurden entworfen, um das Erzeugen von Eingaben für Verarbeiter zu erleichtern, vor allem für Maschinen für reguläre Ausdrücke, die die Maskierung mit Rückwärtsschrägstrichen selbst übernehmen wollen. Solche Verarbeiter betrachten einen ungepaarten abschließenden Rückwärtsschrägstrich ohnehin als Fehler, deshalb verbieten rohe Zeichenketten das. Im Gegenzug erlauben sie dir, das Anführungszeichen der Zeichenkette weiterzugeben, indem du es mit einem Rückwärtsschrägstrich maskierst. Diese Regeln funktionieren gut, wenn r-Strings für ihren eigentlichen Zweck verwendet werden.
Wenn du versuchst, Windows-Pfadnamen zu bauen, beachte, dass alle Systemaufrufe von Windows auch Vorwärtsschrägstriche annehmen:
f = open("/mydir/file.txt") # funktioniert einwandfrei!
Wenn du versuchst, einen Pfadnamen für einen DOS-Befehl zu bauen, versuche zum Beispiel eines von
dir = r"\this\is\my\dos\dir" "\\"
dir = r"\this\is\my\dos\dir\ "[:-1]
dir = "\\this\\is\\my\\dos\\dir\\"
Warum hat Python keine „with“-Anweisung für Attributzuweisungen?¶
Python hat eine with-Anweisung, die die Ausführung eines Blocks umschließt und beim Eintritt in den Block und beim Verlassen Code aufruft. Manche Sprachen haben ein Konstrukt, das so aussieht:
with obj:
a = 1 # entspricht obj.a = 1
total = total + 1 # obj.total = obj.total + 1
In Python wäre ein solches Konstrukt zweideutig.
Andere Sprachen wie Object Pascal, Delphi und C++ verwenden statische Typen, deshalb lässt sich dort eindeutig feststellen, welchem Mitglied zugewiesen wird. Das ist der wesentliche Punkt statischer Typisierung – der Compiler kennt den Gültigkeitsbereich jeder Variablen zur Übersetzungszeit immer.
Python verwendet dynamische Typen. Es ist unmöglich, im Voraus zu wissen, welches Attribut zur Laufzeit angesprochen wird. Attribute können Objekten im laufenden Betrieb hinzugefügt oder entfernt werden. Dadurch lässt sich beim bloßen Lesen nicht feststellen, welches Attribut angesprochen wird: ein lokales, ein globales oder ein Attribut des Objekts?
Nimm zum Beispiel das folgende unvollständige Codestück:
def foo(a):
with a:
print(x)
Das Codestück setzt voraus, dass a ein Attribut namens x haben muss. Es gibt in Python aber nichts, was dem Interpreter das sagt. Was sollte passieren, wenn a, sagen wir, eine ganze Zahl ist? Und wenn es eine globale Variable namens x gibt, wird sie dann innerhalb des with-Blocks verwendet? Wie du siehst, macht die dynamische Natur von Python solche Entscheidungen deutlich schwerer.
Der wichtigste Vorteil von with und ähnlichen Sprachmitteln, nämlich weniger Code, lässt sich in Python aber leicht durch eine Zuweisung erreichen. Statt:
function(args).mydict[index][index].a = 21
function(args).mydict[index][index].b = 42
function(args).mydict[index][index].c = 63
schreibe dies:
ref = function(args).mydict[index][index]
ref.a = 21
ref.b = 42
ref.c = 63
Das hat auch den Nebeneffekt, die Ausführung zu beschleunigen, denn Namensbindungen werden in Python zur Laufzeit aufgelöst, und die zweite Fassung muss die Auflösung nur einmal durchführen.
Ähnliche Vorschläge, die Syntax einführen würden, um die Menge an Code weiter zu verringern, etwa über einen führenden Punkt, wurden zugunsten der Ausdrücklichkeit abgelehnt (siehe https://mail.python.org/pipermail/python-ideas/2016-May/040070.html).
Warum unterstützen Generatoren die with-Anweisung nicht?¶
Aus technischen Gründen würde ein Generator, der direkt als Kontextmanager verwendet wird, nicht richtig funktionieren. Wird ein Generator wie meist als Iterator bis zum Ende durchlaufen, ist kein Schließen nötig. Ist es doch nötig, hülle ihn in der with-Anweisung als contextlib.closing(generator) ein.
Warum sind bei den Anweisungen if/while/def/class Doppelpunkte nötig?¶
Der Doppelpunkt ist vor allem nötig, um die Lesbarkeit zu verbessern — eines der Ergebnisse der experimentellen Sprache ABC. Sieh dir dies an:
if a == b
print(a)
gegenüber
if a == b:
print(a)
Beachte, wie das zweite etwas leichter zu lesen ist. Beachte außerdem, wie ein Doppelpunkt das Beispiel in dieser FAQ-Antwort einleitet. Das ist im Deutschen wie im Englischen üblich.
Ein weiterer, kleinerer Grund ist, dass der Doppelpunkt es Editoren mit Syntaxhervorhebung leichter macht. Sie können nach Doppelpunkten suchen, um zu entscheiden, wann die Einrückung erhöht werden muss, statt den Programmtext aufwendiger zerlegen zu müssen.
Warum erlaubt Python Kommas am Ende von Listen und Tupeln?¶
Python erlaubt dir, am Ende von Listen, Tupeln und Dictionarys ein abschließendes Komma zu setzen:
[1, 2, 3,]
('a', 'b', 'c',)
d = {
"A": [1, 5],
"B": [6, 7], # das letzte abschließende Komma ist optional, aber guter Stil
}
Es gint mehrere Gründe dies zu erlauben.
Hast du einen literalen Wert für eine Liste, ein Tupel oder ein Dictionary über mehrere Zeilen verteilt, ist es leichter, weitere Elemente hinzuzufügen, weil du nicht daran denken musst, in der vorherigen Zeile ein Komma zu ergänzen. Die Zeilen lassen sich außerdem umsortieren, ohne einen Syntaxfehler zu erzeugen.
Das Komma versehentlich weglassen kann zu Fehlern führen, die schwer zu finden sind. Zum Beispiel:
x = [
"fee",
"fie"
"foo",
"fum"
]
Diese Liste sieht aus, als hätte sie vier Elemente, tatsächlich enthält sie aber drei: „fee“, „fiefoo“ und „fum“. Das Komma immer zu setzen vermeidet diese Fehlerquelle.
Ein abschließendes Komma zu erlauben kann außerdem das programmgesteuerte Erzeugen von Code erleichtern.