Design und History FAQ¶
Warum verwendet Python Einrückungen zur Gruppierung von Anweisungen?¶
Guido van Rossum glaubt, dass die Verwendung von Einrückungen zur Gruppierung äußerst elegant ist und viel zur Klarheit des durchschnittlichen Python-Programms beiträgt. Die meisten Leute gewöhnen sich nach einer Weile an diese Funktion.
Da es keine begin/end-Klammern gibt, kann es keine Diskrepanz zwischen der vom Parser wahrgenommenen Gruppierung und der des menschlichen Lesers geben. Gelegentlich stoßen C-Programmierer auf ein Codefragment wie dieses
if (x <= y)
x++;
y--;
z++;
Nur die x++-Anweisung wird ausgeführt, wenn die Bedingung wahr ist, aber die Einrückung lässt viele glauben, dass dies anders ist. Selbst erfahrene C-Programmierer starren manchmal lange darauf und fragen sich, warum y auch für x > y dekrementiert wird.
Da es keine begin/end-Klammern gibt, ist Python viel weniger anfällig für Konflikte im Programmierstil. In C gibt es viele verschiedene Möglichkeiten, die Klammern zu setzen. Nachdem man sich an das Lesen und Schreiben von Code in einem bestimmten Stil gewöhnt hat, ist es normal, sich beim Lesen (oder Schreiben müssen) in einem anderen Stil etwas unwohl zu fühlen.
Viele Programmierstile setzen begin/end-Klammern auf eine eigene Zeile. Dies macht Programme erheblich länger und verschwendet wertvollen Bildschirmplatz, was es schwieriger macht, einen guten Überblick über ein Programm zu bekommen. Idealerweise sollte eine Funktion auf einen Bildschirm passen (sagen wir 20–30 Zeilen). 20 Zeilen Python können viel mehr Arbeit leisten als 20 Zeilen C. Dies ist nicht nur auf das Fehlen von begin/end-Klammern zurückzuführen – das Fehlen von Deklarationen und die hochrangigen Datentypen sind ebenfalls verantwortlich –, aber die einrückungsbasierte Syntax hilft sicherlich.
Warum erhalte ich seltsame Ergebnisse bei einfachen arithmetischen Operationen?¶
Siehe nächste Frage.
Warum sind Gleitkomma-Berechnungen so ungenau?¶
Benutzer sind oft überrascht von Ergebnissen wie diesem
>>> 1.2 - 1.0
0.19999999999999996
und denken, es sei ein Fehler in Python. Das ist es nicht. Das hat wenig mit Python zu tun und viel mehr damit, wie die zugrunde liegende Plattform Gleitkommazahlen behandelt.
Der float-Typ in CPython verwendet ein C double zur Speicherung. Der Wert eines float-Objekts wird in binärem Gleitkomma mit fester Genauigkeit (typischerweise 53 Bit) gespeichert, und Python verwendet C-Operationen, die wiederum auf der Hardware-Implementierung im Prozessor basieren, um Gleitkomma-Operationen durchzuführen. Das bedeutet, dass Python, was Gleitkomma-Operationen angeht, wie viele beliebte Sprachen einschließlich C und Java funktioniert.
Viele Zahlen, die sich leicht in Dezimalnotation schreiben lassen, können nicht exakt in binärer Gleitkommadarstellung ausgedrückt werden. Zum Beispiel, nach
>>> x = 1.2
ist der gespeicherte Wert für x eine (sehr gute) Annäherung an den Dezimalwert 1.2, aber nicht exakt gleich damit. Auf einem typischen Rechner ist der tatsächlich gespeicherte Wert
1.0011001100110011001100110011001100110011001100110011 (binary)
was genau
1.1999999999999999555910790149937383830547332763671875 (decimal)
Die typische Genauigkeit von 53 Bit bietet Python-Floats eine Genauigkeit von 15–16 Dezimalstellen.
Eine ausführlichere Erklärung finden Sie im Kapitel Gleitkomma-Arithmetik im Python-Tutorial.
Warum sind Python-Strings unveränderlich?¶
Es gibt mehrere Vorteile.
Einer davon ist die Leistung: Da bekannt ist, dass ein String unveränderlich ist, können wir zur Erstellungszeit Speicher dafür zuweisen, und die Speicheranforderungen sind fest und unveränderlich. Dies ist auch einer der Gründe für die Unterscheidung zwischen Tupeln und Listen.
Ein weiterer Vorteil ist, dass Strings in Python als „elementar“ wie Zahlen betrachtet werden. Keine Aktivität wird den Wert 8 in etwas anderes ändern, und in Python wird keine Aktivität den String „acht“ in etwas anderes ändern.
Warum muss ‚self‘ in Methodendefinitionen und Aufrufen explizit verwendet werden?¶
Die Idee wurde von Modula-3 übernommen. Sie erweist sich aus verschiedenen Gründen als sehr nützlich.
Erstens ist es offensichtlicher, dass Sie eine Methode oder ein Instanzattribut anstelle einer lokalen Variablen verwenden. Wenn man self.x oder self.meth() liest, ist klar, dass eine Instanzvariable oder -methode verwendet wird, auch wenn man die Klassendefinition nicht auswendig kennt. In C++ kann man das anhand des Fehlens einer lokalen Variablendeklaration erkennen (vorausgesetzt, globale Variablen sind selten oder leicht erkennbar) – aber in Python gibt es keine lokalen Variablendeklarationen, sodass man die Klassendefinition nachschlagen müsste, um sicher zu sein. Einige C++- und Java-Programmierstandards sehen für Instanzattribute ein m_-Präfix vor, sodass diese Explizitheit auch in diesen Sprachen nützlich ist.
Zweitens bedeutet dies, dass keine spezielle Syntax erforderlich ist, wenn Sie explizit auf eine Methode einer bestimmten Klasse verweisen oder sie aufrufen möchten. In C++ müssen Sie den ::-Operator verwenden, wenn Sie eine Methode aus einer Basisklasse verwenden möchten, die in einer abgeleiteten Klasse überschrieben wurde – in Python können Sie baseclass.methodname(self, <argument list>) schreiben. Dies ist besonders nützlich für __init__()-Methoden und im Allgemeinen in Fällen, in denen eine abgeleitete Klassenmethode die Basisklassenmethode mit demselben Namen erweitern möchte und daher die Basisklassenmethode irgendwie aufrufen muss.
Schließlich löst es für Instanzvariablen ein syntaktisches Problem mit der Zuweisung: Da lokale Variablen in Python (per Definition!) diejenigen Variablen sind, denen in einem Funktionskörper ein Wert zugewiesen wird (und die nicht explizit als global deklariert sind), muss es eine Möglichkeit geben, dem Interpreter mitzuteilen, dass eine Zuweisung als Instanzvariable und nicht als lokale Variable gedacht war, und das sollte vorzugsweise syntaktisch sein (aus Effizienzgründen). C++ macht dies durch Deklarationen, aber Python hat keine Deklarationen und es wäre schade, diese nur zu diesem Zweck einführen zu müssen. Die explizite Verwendung von self.var löst dies elegant. Ebenso bedeutet die Notwendigkeit, self.var für die Verwendung von Instanzvariablen zu schreiben, dass Referenzen auf nicht qualifizierte Namen innerhalb einer Methode nicht die Verzeichnisse der Instanz durchsuchen müssen. Anders ausgedrückt, lokale Variablen und Instanzvariablen leben in zwei verschiedenen Namensräumen, und Sie müssen Python mitteilen, welchen Namensraum es verwenden soll.
Warum kann ich eine Zuweisung nicht in einem Ausdruck verwenden?¶
Ab Python 3.8 können Sie das!
Zuweisungsausdrücke mit dem Walross-Operator := weisen einer Variablen in einem Ausdruck einen Wert zu.
while chunk := fp.read(200):
print(chunk)
Weitere Informationen finden Sie in PEP 572.
Warum verwendet Python Methoden für einige Funktionalitäten (z. B. list.index()), aber Funktionen für andere (z. B. len(list))?¶
Wie Guido sagte
(a) Bei einigen Operationen liest sich die Präfixnotation besser als die Postfix-Notation – Präfix- (und Infix-)Operationen haben eine lange Tradition in der Mathematik, die Notationen mag, bei denen die Bilder dem Mathematiker beim Nachdenken über ein Problem helfen. Vergleichen Sie die Leichtigkeit, mit der wir eine Formel wie x*(a+b) in x*a + x*b umschreiben, mit der Ungeschicklichkeit, dasselbe mit einer reinen OO-Notation zu tun.
(b) Wenn ich Code lese, der len(x) sagt, weiß ich, dass die Länge von etwas abgefragt wird. Das sagt mir zwei Dinge: Das Ergebnis ist eine Ganzzahl und das Argument ist eine Art Container. Im Gegensatz dazu muss ich, wenn ich x.len() lese, bereits wissen, dass x eine Art Container ist, der eine Schnittstelle implementiert oder von einer Klasse erbt, die eine Standardlänge hat. Zeugen Sie die Verwirrung, die wir gelegentlich haben, wenn eine Klasse, die keine Zuordnung implementiert, eine get()- oder keys()-Methode hat oder etwas, das keine Datei ist, eine write()-Methode hat.
—https://mail.python.org/pipermail/python-3000/2006-November/004643.html
Warum ist join() eine String-Methode und keine Listen- oder Tupel-Methode?¶
Strings wurden ab Python 1.6 stärker anderen Standardtypen ähneln, als Methoden hinzugefügt wurden, die die gleiche Funktionalität bieten, die schon immer über die Funktionen des String-Moduls verfügbar war. Die meisten dieser neuen Methoden wurden weithin akzeptiert, aber diejenige, die einige Programmierer zu stören scheint, ist
", ".join(['1', '2', '4', '8', '16'])
was das Ergebnis ergibt
"1, 2, 4, 8, 16"
Es gibt zwei gängige Argumente dagegen.
Das erste besagt: „Es sieht wirklich hässlich aus, eine Methode eines String-Literals (Zeichenkettenkonstante) zu verwenden“, worauf die Antwort lautet, dass es das mag, aber ein String-Literal ist nur ein fester Wert. Wenn die Methoden auf Namen, die an Strings gebunden sind, angewendet werden dürfen, gibt es keinen logischen Grund, sie auf Literale nicht anwendbar zu machen.
Der zweite Einwand wird typischerweise so formuliert: „Ich sage eigentlich einer Sequenz, sie soll ihre Elemente mit einer Zeichenkettenkonstante verbinden.“ Traurigerweise tun Sie das nicht. Aus irgendeinem Grund scheint es weniger Schwierigkeiten zu geben, split() als String-Methode zu haben, da es in diesem Fall leicht zu erkennen ist, dass
"1, 2, 4, 8, 16".split(", ")
eine Anweisung an ein String-Literal ist, die Teilzeichenketten zurückzugeben, die durch den gegebenen Trennzeichen (oder standardmäßig durch beliebige Leerzeichenfolgen) getrennt sind.
join() ist eine String-Methode, weil Sie damit der Trennzeichen-String anweisen, eine Sequenz von Strings zu durchlaufen und sich selbst zwischen benachbarte Elemente einzufügen. Diese Methode kann mit jedem Argument verwendet werden, das den Regeln für Sequenzobjekte folgt, einschließlich neuer Klassen, die Sie selbst definieren könnten. Ähnliche Methoden existieren für Bytes- und Bytearray-Objekte.
Wie schnell sind Ausnahmen?¶
Ein try/except-Block ist extrem effizient, wenn keine Ausnahmen ausgelöst werden. Tatsächlich eine Ausnahme abzufangen ist teuer. In Versionen von Python vor 2.0 war es üblich, dieses Idiom zu verwenden
try:
value = mydict[key]
except KeyError:
mydict[key] = getvalue(key)
value = mydict[key]
Dies machte nur Sinn, wenn man davon ausging, dass das Dictionary den Schlüssel fast immer hatte. Wenn dies nicht der Fall war, hat man es so kodiert
if key in mydict:
value = mydict[key]
else:
value = mydict[key] = getvalue(key)
Für diesen speziellen Fall könnten Sie auch value = dict.setdefault(key, getvalue(key)) verwenden, aber nur, wenn der Aufruf getvalue() billig genug ist, da er in allen Fällen ausgewertet wird.
Warum gibt es in Python keine switch- oder case-Anweisung?¶
Im Allgemeinen führen strukturierte Switch-Anweisungen einen Codeblock aus, wenn ein Ausdruck einen bestimmten Wert oder eine Menge von Werten hat. Seit Python 3.10 kann man Literale oder Konstanten innerhalb eines Namensraums mit einer match ... case-Anweisung leicht abgleichen. Eine ältere Alternative ist eine Sequenz von if... elif... elif... else.
Für Fälle, in denen Sie aus einer sehr großen Anzahl von Möglichkeiten auswählen müssen, können Sie ein Dictionary erstellen, das Fallwerte Funktionen zuweist, die aufgerufen werden sollen. Zum Beispiel
functions = {'a': function_1,
'b': function_2,
'c': self.method_1}
func = functions[value]
func()
Für den Aufruf von Methoden auf Objekten können Sie durch die Verwendung der eingebauten Funktion getattr() zur Abfrage von Methoden mit einem bestimmten Namen noch weiter vereinfachen
class MyVisitor:
def visit_a(self):
...
def dispatch(self, value):
method_name = 'visit_' + str(value)
method = getattr(self, method_name)
method()
Es wird empfohlen, ein Präfix für die Methodennamen zu verwenden, z. B. visit_ in diesem Beispiel. Ohne ein solches Präfix, wenn Werte aus einer nicht vertrauenswürdigen Quelle stammen, könnte ein Angreifer jede Methode Ihres Objekts aufrufen.
Das Nachahmen von Switch mit Fallthrough, wie bei C's switch-case-default, ist möglich, viel schwieriger und weniger notwendig.
Kann man Threads nicht im Interpreter emulieren, anstatt sich auf eine betriebssystemspezifische Thread-Implementierung zu verlassen?¶
Antwort 1: Leider schiebt der Interpreter mindestens einen C-Stack-Frame für jeden Python-Stack-Frame. Außerdem können Erweiterungen fast zu zufälligen Zeitpunkten zurück zu Python aufrufen. Daher erfordert eine vollständige Thread-Implementierung Thread-Unterstützung für C.
Antwort 2: Glücklicherweise gibt es Stackless Python, das eine komplett neu gestaltete Interpreter-Schleife hat, die den C-Stack vermeidet.
Warum dürfen Lambda-Ausdrücke keine Anweisungen enthalten?¶
Python Lambda-Ausdrücke dürfen keine Anweisungen enthalten, da das syntaktische Framework von Python keine in Ausdrücken verschachtelten Anweisungen verarbeiten kann. In Python ist dies jedoch kein ernsthaftes Problem. Im Gegensatz zu Lambda-Formen in anderen Sprachen, wo sie Funktionalität hinzufügen, sind Python-Lambdas nur eine Kurzschreibweise, wenn Sie zu faul sind, eine Funktion zu definieren.
Funktionen sind bereits First-Class-Objekte in Python und können in einem lokalen Gültigkeitsbereich deklariert werden. Daher ist der einzige Vorteil der Verwendung eines Lambda anstelle einer lokal definierten Funktion, dass Sie keinen Namen für die Funktion erfinden müssen – aber das ist nur eine lokale Variable, der das Funktions-Objekt (das genau die gleiche Art von Objekt ist, wie ein Lambda-Ausdruck ergibt) zugewiesen wird!
Kann Python in Maschinencode, C oder eine andere Sprache kompiliert werden?¶
Cython kompiliert eine modifizierte Version von Python mit optionalen Annotationen in C-Erweiterungen. Nuitka ist ein aufstrebender Compiler von Python in C++-Code, der darauf abzielt, die vollständige Python-Sprache zu unterstützen.
Wie verwaltet Python den Speicher?¶
Die Details der Python-Speicherverwaltung hängen von der Implementierung ab. Die Standardimplementierung von Python, CPython, verwendet Referenzzählung, um unzugängliche Objekte zu erkennen, und einen weiteren Mechanismus zur Sammlung von Referenzzyklen, der periodisch einen Zykluserkennungsalgorithmus ausführt, der nach unzugänglichen Zyklen sucht und die beteiligten Objekte löscht. Das Modul gc bietet Funktionen zur Durchführung einer Garbage Collection, zum Erhalt von Debugging-Statistiken und zur Feinabstimmung der Parameter des Collectors.
Andere Implementierungen (wie Jython oder PyPy) können sich jedoch auf einen anderen Mechanismus wie einen vollwertigen Garbage Collector verlassen. Dieser Unterschied kann zu subtilen Portierungsproblemen führen, wenn Ihr Python-Code vom Verhalten der Referenzzählungs-Implementierung abhängt.
In einigen Python-Implementierungen läuft der folgende Code (der in CPython in Ordnung ist) wahrscheinlich mit aufgebrauchten Dateideskriptoren
for file in very_long_list_of_files:
f = open(file)
c = f.read(1)
Tatsächlich schließt mit der Referenzzählung und dem Destruktor-Schema von CPython jede neue Zuweisung an f die vorherige Datei. Mit einem traditionellen GC werden diese Datei-Objekte jedoch nur mit variierenden und möglicherweise langen Intervallen gesammelt (und geschlossen).
Wenn Sie Code schreiben möchten, der mit jeder Python-Implementierung funktioniert, sollten Sie die Datei explizit schließen oder die with-Anweisung verwenden; dies funktioniert unabhängig vom Speicherverwaltungsschema
for file in very_long_list_of_files:
with open(file) as f:
c = f.read(1)
Warum verwendet CPython kein traditionelleres Garbage-Collection-Schema?¶
Zum einen ist dies keine Standardfunktion von C und daher nicht portabel. (Ja, wir wissen von der Boehm GC-Bibliothek. Sie enthält Teile von Assembler-Code für *die meisten* gängigen Plattformen, aber nicht für alle, und obwohl sie größtenteils transparent ist, ist sie nicht vollständig transparent; Patches sind erforderlich, damit Python damit funktioniert.)
Ein traditioneller GC wird auch zu einem Problem, wenn Python in andere Anwendungen eingebettet wird. Während in einem eigenständigen Python die Standard- malloc() und free() mit Versionen aus der GC-Bibliothek ersetzt werden können, möchte eine Anwendung, die Python einbettet, möglicherweise *ihren eigenen* Ersatz für malloc() und free() haben und möchte nicht den von Python. Derzeit funktioniert CPython mit allem, was malloc() und free() ordnungsgemäß implementiert.
Warum wird nicht der gesamte Speicher freigegeben, wenn CPython beendet wird?¶
Objekte, auf die aus den globalen Namensräumen von Python-Modulen verwiesen wird, werden beim Beenden von Python nicht immer freigegeben. Dies kann passieren, wenn es zirkuläre Referenzen gibt. Es gibt auch bestimmte Speicherbereiche, die von der C-Bibliothek zugewiesen werden und unmöglich freigegeben werden können (z. B. ein Tool wie Purify wird diese beanstanden). Python ist jedoch aggressiv beim Aufräumen von Speicher beim Beenden und versucht, jedes einzelne Objekt zu zerstören.
Wenn Sie Python zwingen möchten, bestimmte Dinge bei der Deallokation zu löschen, verwenden Sie das Modul atexit, um eine Funktion auszuführen, die diese Löschungen erzwingt.
Warum gibt es separate Tupel- und Listen-Datentypen?¶
Listen und Tupel, obwohl in vielerlei Hinsicht ähnlich, werden im Allgemeinen auf grundsätzlich unterschiedliche Weise verwendet. Tupel können als ähnlich zu Pascal records oder C structs betrachtet werden; sie sind kleine Sammlungen zusammengehöriger Daten, die unterschiedliche Typen haben können und als Gruppe behandelt werden. Zum Beispiel wird ein kartesischer Koordinatenpunkt durch ein Tupel aus zwei oder drei Zahlen angemessen dargestellt.
Listen hingegen ähneln eher Arrays in anderen Sprachen. Sie enthalten tendenziell eine variable Anzahl von Objekten, die alle vom gleichen Typ sind und einzeln verarbeitet werden. Zum Beispiel gibt os.listdir('.') eine Liste von Strings zurück, die die Dateien im aktuellen Verzeichnis darstellen. Funktionen, die diese Ausgabe verarbeiten, würden im Allgemeinen nicht fehlschlagen, wenn Sie dem Verzeichnis weitere ein oder zwei Dateien hinzufügen.
Tupel sind unveränderlich, was bedeutet, dass, sobald ein Tupel erstellt wurde, Sie keines seiner Elemente durch einen neuen Wert ersetzen können. Listen sind veränderlich, was bedeutet, dass Sie die Elemente einer Liste jederzeit ändern können. Nur unveränderliche Elemente können als Dictionary-Schlüssel verwendet werden, und daher können nur Tupel und keine Listen als Schlüssel verwendet werden.
Wie werden Listen in CPython implementiert?¶
CPython-Listen sind tatsächlich Arrays variabler Länge, keine Lisp-ähnlichen verketteten Listen. Die Implementierung verwendet ein zusammenhängendes Array von Referenzen auf andere Objekte und speichert einen Zeiger auf dieses Array und die Länge des Arrays in einer Listen-Kopfstruktur.
Dies macht das Indizieren einer Liste a[i] zu einer Operation, deren Kosten unabhängig von der Größe der Liste oder dem Wert des Index sind.
Beim Anhängen oder Einfügen von Elementen wird das Array von Referenzen vergrößert. Es werden einige clevere Techniken angewendet, um die Leistung beim wiederholten Anhängen von Elementen zu verbessern; wenn das Array vergrößert werden muss, wird zusätzlicher Speicher zugewiesen, sodass die nächsten paar Mal keine tatsächliche Größenänderung erforderlich ist.
Wie werden Dictionaries in CPython implementiert?¶
CPython-Dictionaries sind als vergrößerbare Hash-Tabellen implementiert. Im Vergleich zu B-Bäumen bietet dies unter den meisten Umständen eine bessere Leistung für Lookups (die mit Abstand häufigste Operation), und die Implementierung ist einfacher.
Dictionaries funktionieren, indem sie für jeden im Dictionary gespeicherten Schlüssel einen Hash-Code unter Verwendung der eingebauten Funktion hash() berechnen. Der Hash-Code variiert stark je nach Schlüssel und einem pro Prozess-Seed; zum Beispiel könnte 'Python' zu -539294296 gehasht werden, während 'python', ein String, der sich um ein einzelnes Bit unterscheidet, zu 1142331976 gehasht werden könnte. Der Hash-Code wird dann verwendet, um eine Position in einem internen Array zu berechnen, an der der Wert gespeichert wird. Unter der Annahme, dass Sie Schlüssel speichern, die alle unterschiedliche Hash-Werte haben, bedeutet dies, dass Dictionaries eine konstante Zeit – *O*(1) in Big-O-Notation – für den Abruf eines Schlüssels benötigen.
Warum müssen Dictionary-Schlüssel unveränderlich sein?¶
Die Hash-Tabellen-Implementierung von Dictionaries verwendet einen Hash-Wert, der aus dem Schlüsselwert berechnet wird, um den Schlüssel zu finden. Wenn der Schlüssel ein veränderliches Objekt wäre, könnte sich sein Wert ändern und damit auch sein Hash. Da jedoch jeder, der das Schlüsselobjekt ändert, nicht erkennen kann, dass es als Dictionary-Schlüssel verwendet wurde, kann er den Eintrag im Dictionary nicht verschieben. Wenn Sie dann versuchen, dasselbe Objekt im Dictionary nachzuschlagen, wird es nicht gefunden, da sein Hash-Wert anders ist. Wenn Sie versuchen, den alten Wert nachzuschlagen, würde er ebenfalls nicht gefunden, da der Wert des Objekts, das in diesem Hash-Bin gefunden wurde, anders wäre.
Wenn Sie ein Dictionary mit einer Liste indizieren möchten, konvertieren Sie die Liste einfach zuerst in ein Tupel; die Funktion tuple(L) erstellt ein Tupel mit denselben Einträgen wie die Liste L. Tupel sind unveränderlich und können daher als Dictionary-Schlüssel verwendet werden.
Einige inakzeptable Lösungsversuche, die vorgeschlagen wurden
Hash-Listen nach ihrer Adresse (Objekt-ID). Das funktioniert nicht, denn wenn Sie eine neue Liste mit demselben Wert konstruieren, wird sie nicht gefunden; z.B.
mydict = {[1, 2]: '12'} print(mydict[[1, 2]])
würde eine
KeyError-Ausnahme auslösen, da die ID der[1, 2]in der zweiten Zeile von der in der ersten Zeile abweicht. Mit anderen Worten, Dictionary-Schlüssel sollten mit==verglichen werden, nicht mitis.Machen Sie eine Kopie, wenn Sie eine Liste als Schlüssel verwenden. Das funktioniert nicht, weil die Liste, als veränderliches Objekt, einen Verweis auf sich selbst enthalten könnte, und dann würde der Kopiercode in eine Endlosschleife geraten.
Erlauben Sie Listen als Schlüssel, sagen Sie dem Benutzer aber, er soll sie nicht modifizieren. Dies würde eine Klasse von schwer nachvollziehbaren Fehlern in Programmen ermöglichen, wenn Sie versehentlich eine Liste vergessen oder modifizieren. Es verletzt auch eine wichtige Invariante von Dictionaries: Jeder Wert in
d.keys()kann als Schlüssel des Dictionaries verwendet werden.Markieren Sie Listen als schreibgeschützt, sobald sie als Dictionary-Schlüssel verwendet werden. Das Problem ist, dass nicht nur das Top-Level-Objekt seinen Wert ändern könnte; Sie könnten ein Tupel verwenden, das eine Liste enthält, als Schlüssel. Das Einfügen von irgendetwas als Schlüssel in ein Dictionary würde erfordern, dass alle von dort erreichbaren Objekte als schreibgeschützt markiert werden – und auch hier könnten selbstverweisende Objekte eine Endlosschleife verursachen.
Es gibt einen Trick, um dies zu umgehen, wenn Sie ihn benötigen, aber verwenden Sie ihn auf eigenes Risiko: Sie können eine veränderliche Struktur in eine Klasseninstanz einpacken, die sowohl eine __eq__()- als auch eine __hash__()-Methode hat. Sie müssen dann sicherstellen, dass der Hash-Wert für alle solchen Wrapper-Objekte, die sich in einem Dictionary (oder einer anderen Hash-basierten Struktur) befinden, unverändert bleibt, solange sich das Objekt im Dictionary (oder einer anderen Struktur) befindet.
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
Beachten Sie, dass die Hash-Berechnung durch die Möglichkeit erschwert wird, dass einige Elemente der Liste nicht hashbar sind, und auch durch die Möglichkeit von Überläufen bei der Arithmetik.
Darüber hinaus muss immer gelten, dass wenn o1 == o2 (d.h. o1.__eq__(o2) ist True), dann hash(o1) == hash(o2) (d. h. o1.__hash__() == o2.__hash__()), unabhängig davon, ob das Objekt in einem Dictionary ist oder nicht. Wenn Sie diese Einschränkungen nicht einhalten, werden Dictionaries und andere Hash-basierte Strukturen Fehlfunktionen aufweisen.
Im Fall von ListWrapper muss die verpackte Liste unverändert bleiben, solange sich das Wrapper-Objekt in einem Dictionary befindet, um Anomalien zu vermeiden. Tun Sie dies nicht, es sei denn, Sie sind bereit, sorgfältig über die Anforderungen und die Konsequenzen der Nichteinhaltung nachzudenken. Betrachten Sie sich als gewarnt.
Warum gibt list.sort() nicht die sortierte Liste zurück?¶
In Situationen, in denen die Leistung wichtig ist, wäre das Erstellen einer Kopie der Liste, nur um sie zu sortieren, Verschwendung. Daher sortiert list.sort() die Liste direkt (in-place). Um Sie daran zu erinnern, gibt es die sortierte Liste nicht zurück. So werden Sie nicht versehentlich eine Liste überschreiben, wenn Sie eine sortierte Kopie benötigen, aber auch die unsortierte Version behalten möchten.
Wenn Sie eine neue Liste zurückgeben möchten, verwenden Sie stattdessen die eingebaute Funktion sorted(). Diese Funktion erstellt eine neue Liste aus einem bereitgestellten Iterable, sortiert sie und gibt sie zurück. Zum Beispiel, hier ist, wie man über die Schlüssel eines Dictionaries in sortierter Reihenfolge iteriert
for key in sorted(mydict):
... # do whatever with mydict[key]...
Wie gibt man eine Schnittstellenspezifikation in Python an und erzwingt sie?¶
Eine Schnittstellenspezifikation für ein Modul, wie sie in Sprachen wie C++ und Java bereitgestellt wird, beschreibt die Prototypen der Methoden und Funktionen des Moduls. Viele sind der Meinung, dass die Laufzeit-Durchsetzung von Schnittstellenspezifikationen beim Erstellen großer Programme hilfreich ist.
Python 2.6 fügt ein abc Modul hinzu, mit dem Sie Abstrakte Basisklassen (ABCs) definieren können. Sie können dann isinstance() und issubclass() verwenden, um zu prüfen, ob eine Instanz oder eine Klasse eine bestimmte ABC implementiert. Das collections.abc Modul definiert eine Reihe nützlicher ABCs wie Iterable, Container und MutableMapping.
Für Python können viele der Vorteile von Schnittstellenspezifikationen durch eine angemessene Testdisziplin für Komponenten erzielt werden.
Eine gute Testsuite für ein Modul kann sowohl einen Regressionstest liefern als auch als Modulschnittstellenspezifikation und als Beispielsammlung dienen. Viele Python-Module können als Skript ausgeführt werden, um einen einfachen "Selbsttest" durchzuführen. Selbst Module, die komplexe externe Schnittstellen verwenden, können oft isoliert getestet werden, indem triviale "Stub"-Emulationen der externen Schnittstelle verwendet werden. Die Module doctest und unittest oder Drittanbieter-Testframeworks können verwendet werden, um erschöpfende Testsuiten zu erstellen, die jede Zeile Code in einem Modul durchlaufen.
Eine angemessene Testdisziplin kann dazu beitragen, große komplexe Anwendungen in Python zu erstellen, genauso wie Schnittstellenspezifikationen es tun würden. Tatsächlich kann es besser sein, da eine Schnittstellenspezifikation bestimmte Eigenschaften eines Programms nicht testen kann. Zum Beispiel 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 Ihre Implementierung von list.append() dies tatsächlich korrekt tut, aber es ist trivial, diese Eigenschaft in einer Testsuite zu überprüfen.
Das Schreiben von Testsuiten ist sehr hilfreich, und Sie möchten vielleicht Ihren Code so gestalten, dass er leicht getestet werden kann. Eine zunehmend beliebte Technik, Test-Driven Development (TDD), fordert dazu auf, Teile der Testsuite zuerst zu schreiben, bevor Sie den eigentlichen Code schreiben. Natürlich erlaubt Python Ihnen, nachlässig zu sein und gar keine Testfälle zu schreiben.
Warum gibt es kein goto?¶
In den 1970er Jahren erkannten die Leute, dass ungezügeltes goto zu unübersichtlichem "Spaghetti"-Code führen konnte, der schwer zu verstehen und zu überarbeiten war. In einer Hochsprache ist es auch unnötig, solange es Möglichkeiten zum Verzweigen gibt (in Python mit if-Anweisungen und or, and und if/else-Ausdrücken) und Schleifen (mit while und for-Anweisungen, möglicherweise mit continue und break).
Man kann auch Ausnahmen verwenden, um ein "strukturiertes goto" bereitzustellen, das auch funktionsübergreifend funktioniert. Viele sind der Meinung, dass Ausnahmen alle sinnvollen Verwendungen der go oder goto Konstrukte von C, Fortran und anderen Sprachen bequem emulieren können. Zum Beispiel
class label(Exception): pass # declare a label
try:
...
if condition: raise label() # goto label
...
except label: # where to goto
pass
...
Dies erlaubt Ihnen nicht, in die Mitte einer Schleife zu springen, aber das wird ohnehin normalerweise als Missbrauch von goto angesehen. Sparsam verwenden.
Warum dürfen Roh-Strings (r-Strings) nicht mit einem Backslash enden?¶
Genauer gesagt, dürfen sie nicht mit einer ungeraden Anzahl von Backslashes enden: Der nicht gepaarte Backslash am Ende maskiert das schließende Anführungszeichen und hinterlässt einen nicht beendeten String.
Roh-Strings wurden entwickelt, um die Erstellung von Eingaben für Prozessoren (hauptsächlich reguläre Ausdrucks-Engines) zu erleichtern, die ihre eigene Backslash-Escaping-Verarbeitung durchführen möchten. Solche Prozessoren betrachten einen nicht übereinstimmenden nachgestellten Backslash ohnehin als Fehler, daher verbieten Roh-Strings dies. Im Gegenzug erlauben sie Ihnen, das Anführungszeichen für Strings durch Maskierung mit einem Backslash zu übergeben. Diese Regeln funktionieren gut, wenn r-Strings für ihren beabsichtigten Zweck verwendet werden.
Wenn Sie Windows-Pfadnamen erstellen, beachten Sie, dass alle Windows-Systemaufrufe auch Vorwärtsschrägstriche akzeptieren.
f = open("/mydir/file.txt") # works fine!
Wenn Sie versuchen, einen Pfadnamen für einen DOS-Befehl zu erstellen, versuchen Sie es z. B. mit einem der folgenden:
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 und Austritt aus dem Block Code aufruft. Einige Sprachen haben ein Konstrukt, das so aussieht
with obj:
a = 1 # equivalent to obj.a = 1
total = total + 1 # obj.total = obj.total + 1
In Python wäre ein solches Konstrukt mehrdeutig.
Andere Sprachen, wie Object Pascal, Delphi und C++, verwenden statische Typen, so dass es möglich ist, eindeutig zu wissen, welchem Member ein Wert zugewiesen wird. Das ist der Hauptpunkt der statischen Typisierung – der Compiler kennt zur Kompilierzeit *immer* den Geltungsbereich jeder Variablen.
Python verwendet dynamische Typen. Es ist unmöglich, im Voraus zu wissen, auf welches Attribut zur Laufzeit zugegriffen wird. Member-Attribute können dynamisch zu Objekten hinzugefügt oder von ihnen entfernt werden. Dies macht es unmöglich, aus einer einfachen Lesung zu wissen, auf welches Attribut zugegriffen wird: ein lokales, ein globales oder ein Member-Attribut?
Nehmen Sie zum Beispiel den folgenden unvollständigen Ausschnitt
def foo(a):
with a:
print(x)
Der Ausschnitt geht davon aus, dass a ein Member-Attribut namens x haben muss. Es gibt jedoch nichts in Python, das dem Interpreter dies mitteilt. Was passiert, wenn a beispielsweise eine Ganzzahl ist? Wenn es eine globale Variable namens x gibt, wird diese dann innerhalb des with-Blocks verwendet? Wie Sie sehen, erschwert die dynamische Natur von Python solche Entscheidungen erheblich.
Der Hauptvorteil von with und ähnlichen Sprachmerkmalen (Reduzierung des Codeumfangs) kann jedoch in Python leicht durch Zuweisung erreicht werden. Anstatt
function(args).mydict[index][index].a = 21
function(args).mydict[index][index].b = 42
function(args).mydict[index][index].c = 63
schreiben Sie dies
ref = function(args).mydict[index][index]
ref.a = 21
ref.b = 42
ref.c = 63
Dies hat auch den Nebeneffekt, dass die Ausführungsgeschwindigkeit erhöht wird, da Namenbindungen in Python zur Laufzeit aufgelöst werden und die zweite Version die Auflösung nur einmal durchführen muss.
Ähnliche Vorschläge, die eine Syntax zur weiteren Reduzierung des Codeumfangs einführen würden, wie z. B. die Verwendung eines 'führenden Punkts', wurden zugunsten der Explizitheit 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 korrekt funktionieren. Wenn, wie am häufigsten, ein Generator als Iterator verwendet wird, der bis zum Ende durchläuft, ist kein Schließen erforderlich. Wenn doch, wrappen Sie ihn als contextlib.closing(generator) in der with-Anweisung.
Warum sind für if/while/def/class-Anweisungen Doppelpunkte erforderlich?¶
Der Doppelpunkt ist hauptsächlich zur Verbesserung der Lesbarkeit erforderlich (eines der Ergebnisse der experimentellen ABC-Sprache). Betrachten Sie dies
if a == b
print(a)
versus
if a == b:
print(a)
Beachten Sie, wie das zweite etwas leichter zu lesen ist. Beachten Sie weiter, wie ein Doppelpunkt das Beispiel in dieser FAQ-Antwort abgrenzt; dies ist eine Standardverwendung im Englischen.
Ein weiterer geringfügiger Grund ist, dass der Doppelpunkt es Editoren mit Syntax-Hervorhebung erleichtert; sie können nach Doppelpunkten suchen, um zu entscheiden, wann die Einrückung erhöht werden muss, anstatt eine aufwendigere Analyse des Programmtextes durchführen zu müssen.
Warum erlaubt Python Kommas am Ende von Listen und Tupeln?¶
Python erlaubt Ihnen, ein nachgestelltes Komma am Ende von Listen, Tupeln und Dictionaries hinzuzufügen
[1, 2, 3,]
('a', 'b', 'c',)
d = {
"A": [1, 5],
"B": [6, 7], # last trailing comma is optional but good style
}
Es gibt mehrere Gründe, dies zu erlauben.
Wenn Sie einen Literalwert für eine Liste, ein Tupel oder ein Dictionary haben, der sich über mehrere Zeilen erstreckt, ist es einfacher, weitere Elemente hinzuzufügen, da Sie nicht vergessen müssen, der vorherigen Zeile ein Komma hinzuzufügen. Die Zeilen können auch neu geordnet werden, ohne einen Syntaxfehler zu erzeugen.
Das versehentliche Weglassen des Kommas kann zu schwer zu diagnostizierenden Fehlern führen. Zum Beispiel
x = [
"fee",
"fie"
"foo",
"fum"
]
Diese Liste sieht so aus, als hätte sie vier Elemente, aber sie enthält tatsächlich drei: „fee“, „fiefoo“ und „fum“. Das ständige Hinzufügen des Kommas vermeidet diese Fehlerquelle.
Das Zulassen des nachgestellten Kommas kann auch die programmatische Code-Generierung erleichtern.