Fehlerhafter Python-Code: Die 10 häufigsten Fehler, die Python-Entwickler machen

Veröffentlicht: 2022-03-11

Über Python

Python ist eine interpretierte, objektorientierte, höhere Programmiersprache mit dynamischer Semantik. Seine integrierten Datenstrukturen auf hoher Ebene, kombiniert mit dynamischer Typisierung und dynamischer Bindung, machen es sehr attraktiv für die schnelle Anwendungsentwicklung sowie für die Verwendung als Skript- oder Glue-Sprache, um vorhandene Komponenten oder Dienste zu verbinden. Python unterstützt Module und Pakete und fördert dadurch die Programmmodularität und die Wiederverwendung von Code.

Über diesen Artikel

Die einfache, leicht zu erlernende Syntax von Python kann Python-Entwickler – insbesondere diejenigen, die neu in der Sprache sind – dazu verleiten, einige ihrer Feinheiten zu übersehen und die Leistungsfähigkeit der vielfältigen Python-Sprache zu unterschätzen.

Vor diesem Hintergrund präsentiert dieser Artikel eine „Top 10“-Liste von etwas subtilen, schwerer zu fangenden Fehlern, die sogar einige fortgeschrittenere Python-Entwickler in den Hintern beißen können.

(Hinweis: Dieser Artikel richtet sich an ein fortgeschritteneres Publikum als Common Mistakes of Python Programmers, das sich eher an diejenigen richtet, die neu in der Sprache sind.)

Häufiger Fehler Nr. 1: Missbrauch von Ausdrücken als Standardwerte für Funktionsargumente

Mit Python können Sie angeben, dass ein Funktionsargument optional ist, indem Sie einen Standardwert dafür angeben. Obwohl dies ein großartiges Feature der Sprache ist, kann es zu einiger Verwirrung führen, wenn der Standardwert mutable ist. Betrachten Sie zum Beispiel diese Python-Funktionsdefinition:

 >>> def foo(bar=[]): # bar is optional and defaults to [] if not specified ... bar.append("baz") # but this line could be problematic, as we'll see... ... return bar

Ein häufiger Fehler ist die Annahme, dass das optionale Argument jedes Mal auf den angegebenen Standardausdruck gesetzt wird, wenn die Funktion aufgerufen wird, ohne einen Wert für das optionale Argument anzugeben. Im obigen Code könnte man beispielsweise erwarten, dass der wiederholte Aufruf von foo() (d. h. ohne Angabe eines bar -Arguments) immer 'baz' zurückgeben würde, da die Annahme wäre, dass jedes Mal , wenn foo() aufgerufen wird (ohne bar Argument angegeben) bar wird auf [] gesetzt (dh eine neue leere Liste).

Aber schauen wir uns an, was tatsächlich passiert, wenn Sie dies tun:

 >>> foo() ["baz"] >>> foo() ["baz", "baz"] >>> foo() ["baz", "baz", "baz"]

Häh? Warum hat es jedes Mal, wenn foo() aufgerufen wurde, den Standardwert von "baz" an eine vorhandene Liste angehängt, anstatt jedes Mal eine neue Liste zu erstellen?

Die fortgeschrittenere Antwort der Python-Programmierung lautet, dass der Standardwert für ein Funktionsargument nur einmal zum Zeitpunkt der Definition der Funktion ausgewertet wird. Daher wird das bar Argument nur dann auf seinen Standardwert (dh eine leere Liste) initialisiert, wenn foo() zuerst definiert wird, aber dann werden Aufrufe von foo() (dh ohne ein bar Argument angegeben) weiterhin dieselbe Liste to verwenden welcher bar ursprünglich initialisiert wurde.

FYI, eine gängige Problemumgehung dafür ist wie folgt:

 >>> def foo(bar=None): ... if bar is None: # or if not bar: ... bar = [] ... bar.append("baz") ... return bar ... >>> foo() ["baz"] >>> foo() ["baz"] >>> foo() ["baz"]

Häufiger Fehler Nr. 2: Falsche Verwendung von Klassenvariablen

Betrachten Sie das folgende Beispiel:

 >>> class A(object): ... x = 1 ... >>> class B(A): ... pass ... >>> class C(A): ... pass ... >>> print Ax, Bx, Cx 1 1 1

Macht Sinn.

 >>> Bx = 2 >>> print Ax, Bx, Cx 1 2 1

Ja, wieder wie erwartet.

 >>> Ax = 3 >>> print Ax, Bx, Cx 3 2 3

Was zum Teufel $%#!& ?? Wir haben nur Ax geändert. Warum hat sich auch Cx geändert?

In Python werden Klassenvariablen intern als Wörterbücher behandelt und folgen dem, was oft als Method Resolution Order (MRO) bezeichnet wird. Da also im obigen Code das Attribut x nicht in Klasse C gefunden wird, wird es in seinen Basisklassen nachgeschlagen (nur A im obigen Beispiel, obwohl Python Mehrfachvererbung unterstützt). Mit anderen Worten, C hat keine eigene x Eigenschaft, unabhängig von A . Somit sind Verweise auf Cx tatsächlich Verweise auf Ax . Dies verursacht ein Python-Problem, wenn es nicht richtig gehandhabt wird. Erfahren Sie mehr über Klassenattribute in Python.

Häufiger Fehler Nr. 3: Falsche Angabe von Parametern für einen Ausnahmeblock

Angenommen, Sie haben den folgenden Code:

 >>> try: ... l = ["a", "b"] ... int(l[2]) ... except ValueError, IndexError: # To catch both exceptions, right? ... pass ... Traceback (most recent call last): File "<stdin>", line 3, in <module> IndexError: list index out of range

Das Problem dabei ist, dass die except Anweisung keine auf diese Weise angegebene Liste von Ausnahmen akzeptiert. Stattdessen wird in Python 2.x die Syntax except Exception, e verwendet, um die Ausnahme an den optionalen zweiten angegebenen Parameter (in diesem Fall e ) zu binden, um sie für eine weitere Untersuchung verfügbar zu machen. Daher wird im obigen Code die IndexError Ausnahme nicht von der except -Anweisung abgefangen; Stattdessen wird die Ausnahme stattdessen an einen Parameter namens IndexError gebunden.

Der richtige Weg, mehrere Ausnahmen in einer except -Anweisung abzufangen, besteht darin, den ersten Parameter als Tupel anzugeben, das alle abzufangenden Ausnahmen enthält. Verwenden Sie für maximale Portabilität auch das Schlüsselwort as , da diese Syntax sowohl von Python 2 als auch von Python 3 unterstützt wird:

 >>> try: ... l = ["a", "b"] ... int(l[2]) ... except (ValueError, IndexError) as e: ... pass ... >>>

Häufiger Fehler Nr. 4: Missverständnis der Geltungsbereichsregeln von Python

Die Auflösung des Python-Bereichs basiert auf der sogenannten LEGB-Regel, die eine Abkürzung für L ocal , Enclosing , Global, B uilt-in ist. Scheint einfach genug, oder? Nun, tatsächlich gibt es einige Feinheiten in der Art und Weise, wie dies in Python funktioniert, was uns zu dem allgemeinen, fortgeschritteneren Python-Programmierproblem unten bringt. Folgendes berücksichtigen:

 >>> x = 10 >>> def foo(): ... x += 1 ... print x ... >>> foo() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in foo UnboundLocalError: local variable 'x' referenced before assignment

Was ist das Problem?

Der obige Fehler tritt auf, weil, wenn Sie eine Zuweisung an eine Variable in einem Gültigkeitsbereich vornehmen, diese Variable von Python automatisch als lokal für diesen Gültigkeitsbereich angesehen wird und jede ähnlich benannte Variable in einem beliebigen äußeren Gültigkeitsbereich überschattet.

Viele sind daher überrascht, einen UnboundLocalError in zuvor funktionierendem Code zu erhalten, wenn dieser geändert wird, indem irgendwo im Hauptteil einer Funktion eine Zuweisungsanweisung hinzugefügt wird. (Hier können Sie mehr darüber lesen.)

Dies führt besonders häufig dazu, dass Entwickler bei der Verwendung von Listen ins Stolpern geraten. Betrachten Sie das folgende Beispiel:

 >>> lst = [1, 2, 3] >>> def foo1(): ... lst.append(5) # This works ok... ... >>> foo1() >>> lst [1, 2, 3, 5] >>> lst = [1, 2, 3] >>> def foo2(): ... lst += [5] # ... but this bombs! ... >>> foo2() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in foo UnboundLocalError: local variable 'lst' referenced before assignment

Häh? Warum hat foo2 bombardiert, während foo1 lief?

Die Antwort ist die gleiche wie im vorherigen Beispielproblem, aber zugegebenermaßen subtiler. foo1 nimmt keine Zuweisung an lst , während foo2 dies tut. Wenn wir uns daran erinnern, dass lst += [5] eigentlich nur eine Abkürzung für lst = lst + [5] ist, sehen wir, dass wir versuchen, lst einen Wert lst (daher wird von Python angenommen, dass es sich im lokalen Bereich befindet). Der Wert, den wir lst zuweisen möchten, basiert jedoch auf lst selbst (auch hier wird jetzt davon ausgegangen, dass es sich im lokalen Gültigkeitsbereich befindet), das noch nicht definiert wurde. Boom.

Häufiger Fehler Nr. 5: Eine Liste ändern, während man darüber iteriert

Das Problem mit dem folgenden Code sollte ziemlich offensichtlich sein:

 >>> odd = lambda x : bool(x % 2) >>> numbers = [n for n in range(10)] >>> for i in range(len(numbers)): ... if odd(numbers[i]): ... del numbers[i] # BAD: Deleting item from a list while iterating over it ... Traceback (most recent call last): File "<stdin>", line 2, in <module> IndexError: list index out of range

Das Löschen eines Elements aus einer Liste oder einem Array, während darüber iteriert wird, ist ein Python-Problem, das jedem erfahrenen Softwareentwickler bekannt ist. Aber während das obige Beispiel ziemlich offensichtlich sein mag, können sogar fortgeschrittene Entwickler in Code, der viel komplexer ist, unbeabsichtigt davon gebissen werden.

Glücklicherweise enthält Python eine Reihe eleganter Programmierparadigmen, die bei richtiger Verwendung zu einem erheblich vereinfachten und optimierten Code führen können. Ein Nebeneffekt davon ist, dass einfacherer Code weniger wahrscheinlich von dem Fehler „versehentliches-Löschen-eines-Listenelements-während-des-Iterierens-darüber“ gebissen wird. Ein solches Paradigma ist das Listenverständnis. Darüber hinaus sind Listenverständnisse besonders nützlich, um dieses spezielle Problem zu vermeiden, wie diese alternative Implementierung des obigen Codes zeigt, die perfekt funktioniert:

 >>> odd = lambda x : bool(x % 2) >>> numbers = [n for n in range(10)] >>> numbers[:] = [n for n in numbers if not odd(n)] # ahh, the beauty of it all >>> numbers [0, 2, 4, 6, 8]

Häufiger Fehler Nr. 6: Verwirrung, wie Python Variablen in Closures bindet

Betrachten Sie das folgende Beispiel:

 >>> def create_multipliers(): ... return [lambda x : i * x for i in range(5)] >>> for multiplier in create_multipliers(): ... print multiplier(2) ...

Sie können die folgende Ausgabe erwarten:

 0 2 4 6 8

Aber eigentlich bekommt man:

 8 8 8 8 8

Überraschung!

Dies geschieht aufgrund des späten Bindungsverhaltens von Python, das besagt, dass die Werte von Variablen, die in Closures verwendet werden, zum Zeitpunkt des Aufrufs der inneren Funktion nachgeschlagen werden. Im obigen Code wird also immer dann, wenn eine der zurückgegebenen Funktionen aufgerufen wird, der Wert von i zum Zeitpunkt des Aufrufs im umgebenden Gültigkeitsbereich nachgeschlagen (und bis dahin ist die Schleife abgeschlossen, sodass i bereits sein Finale zugewiesen wurde Wert 4).

Die Lösung für dieses allgemeine Python-Problem ist ein kleiner Hack:

 >>> def create_multipliers(): ... return [lambda x, i=i : i * x for i in range(5)] ... >>> for multiplier in create_multipliers(): ... print multiplier(2) ... 0 2 4 6 8

Voila! Wir nutzen hier Standardargumente, um anonyme Funktionen zu generieren, um das gewünschte Verhalten zu erreichen. Manche würden das elegant nennen. Manche würden es subtil nennen. Manche hassen es. Aber wenn Sie ein Python-Entwickler sind, ist es auf jeden Fall wichtig, es zu verstehen.

Häufiger Fehler Nr. 7: Erstellen von zirkulären Modulabhängigkeiten

Angenommen, Sie haben zwei Dateien, a.py und b.py , die jeweils die andere wie folgt importieren:

In a.py :

 import b def f(): return bx print f()

Und in b.py :

 import a x = 1 def g(): print af()

Versuchen wir zunächst, a.py zu importieren:

 >>> import a 1

Hat gut funktioniert. Vielleicht überrascht Sie das. Immerhin haben wir hier einen zirkulären Import, was vermutlich ein Problem sein sollte, oder?

Die Antwort ist, dass das bloße Vorhandensein eines zirkulären Imports an und für sich kein Problem in Python ist. Wenn ein Modul bereits importiert wurde, ist Python schlau genug, nicht zu versuchen, es erneut zu importieren. Abhängig von dem Punkt, an dem jedes Modul versucht, auf Funktionen oder Variablen zuzugreifen, die im anderen definiert sind, können Sie jedoch tatsächlich auf Probleme stoßen.

Zurück zu unserem Beispiel: Als wir a.py importierten, gab es beim Importieren von b.py keine Probleme, da b.py zum Zeitpunkt des Imports nichts von a.py definieren muss. Der einzige Verweis in b.py auf a ist der Aufruf von af() . Aber dieser Aufruf ist in g() und nichts in a.py oder b.py ruft g() auf. Das Leben ist also gut.

Aber was passiert, wenn wir versuchen, b.py zu importieren (ohne vorher a.py importiert zu haben, das heißt):

 >>> import b Traceback (most recent call last): File "<stdin>", line 1, in <module> File "b.py", line 1, in <module> import a File "a.py", line 6, in <module> print f() File "a.py", line 4, in f return bx AttributeError: 'module' object has no attribute 'x'

Uh-oh. Das ist nicht gut! Das Problem dabei ist, dass beim Importieren von b.py versucht wird, a.py zu importieren, was wiederum f() aufruft, das versucht, auf bx zuzugreifen. Aber bx wurde noch nicht definiert. Daher die AttributeError Ausnahme.

Zumindest eine Lösung dafür ist ziemlich trivial. Ändern Sie einfach b.py , um a.py in g() zu importieren:

 x = 1 def g(): import a # This will be evaluated only when g() is called print af()

Nein, wenn wir es importieren, ist alles in Ordnung:

 >>> import b >>> bg() 1 # Printed a first time since module 'a' calls 'print f()' at the end 1 # Printed a second time, this one is our call to 'g'

Häufiger Fehler Nr. 8: Namenskonflikte mit Modulen der Python-Standardbibliothek

Eine der Schönheiten von Python ist die Fülle an Bibliotheksmodulen, die es „out of the box“ enthält. Aber wenn Sie es nicht bewusst vermeiden, ist es daher nicht so schwierig, auf einen Namenskonflikt zwischen dem Namen eines Ihrer Module und einem Modul mit demselben Namen in der Standardbibliothek zu stoßen, die mit Python geliefert wird (z , haben Sie möglicherweise ein Modul namens email.py in Ihrem Code, das im Konflikt mit dem gleichnamigen Standardbibliotheksmodul stehen würde).

Dies kann zu schwerwiegenden Problemen führen, z. B. beim Importieren einer anderen Bibliothek, die wiederum versucht, die Version der Python-Standardbibliothek eines Moduls zu importieren, aber da Sie ein Modul mit demselben Namen haben, importiert das andere Paket fälschlicherweise Ihre Version anstelle der darin enthaltenen die Python-Standardbibliothek. Hier passieren schlimme Python-Fehler.

Es sollte daher darauf geachtet werden, nicht die gleichen Namen wie in den Modulen der Python-Standardbibliothek zu verwenden. Es ist viel einfacher für Sie, den Namen eines Moduls in Ihrem Paket zu ändern, als einen Python Enhancement Proposal (PEP) einzureichen, um eine Namensänderung im Upstream anzufordern und zu versuchen, diese Genehmigung zu erhalten.

Häufiger Fehler Nr. 9: Unterschiede zwischen Python 2 und Python 3 nicht ansprechen

Betrachten Sie die folgende Datei foo.py :

 import sys def bar(i): if i == 1: raise KeyError(1) if i == 2: raise ValueError(2) def bad(): e = None try: bar(int(sys.argv[1])) except KeyError as e: print('key error') except ValueError as e: print('value error') print(e) bad()

Auf Python 2 läuft das gut:

 $ python foo.py 1 key error 1 $ python foo.py 2 value error 2

Aber jetzt versuchen wir es mal mit Python 3:

 $ python3 foo.py 1 key error Traceback (most recent call last): File "foo.py", line 19, in <module> bad() File "foo.py", line 17, in bad print(e) UnboundLocalError: local variable 'e' referenced before assignment

Was ist hier gerade passiert? Das „Problem“ besteht darin, dass in Python 3 das except nicht über den Geltungsbereich des Except-Blocks hinaus zugänglich ist. (Der Grund dafür ist, dass sonst ein Referenzzyklus mit dem Stack-Frame im Speicher bleiben würde, bis der Garbage Collector ausgeführt wird und die Referenzen aus dem Speicher löscht. Weitere technische Details dazu finden Sie hier).

Eine Möglichkeit, dieses Problem zu vermeiden, besteht darin, einen Verweis auf das except außerhalb des Gültigkeitsbereichs des Except-Blocks beizubehalten, damit es zugänglich bleibt. Hier ist eine Version des vorherigen Beispiels, die diese Technik verwendet und dadurch Code ergibt, der sowohl für Python 2 als auch für Python 3 geeignet ist:

 import sys def bar(i): if i == 1: raise KeyError(1) if i == 2: raise ValueError(2) def good(): exception = None try: bar(int(sys.argv[1])) except KeyError as e: exception = e print('key error') except ValueError as e: exception = e print('value error') print(exception) good()

Führen Sie dies auf Py3k aus:

 $ python3 foo.py 1 key error 1 $ python3 foo.py 2 value error 2

Hurra!

(Übrigens erörtert unser Leitfaden zur Einstellung von Python eine Reihe weiterer wichtiger Unterschiede, die Sie bei der Migration von Code von Python 2 zu Python 3 beachten sollten.)

Häufiger Fehler Nr. 10: Missbrauch der Methode __del__

Nehmen wir an, Sie hatten dies in einer Datei namens mod.py :

 import foo class Bar(object): ... def __del__(self): foo.cleanup(self.myhandle)

Und Sie haben dann versucht, dies von another_mod.py aus zu tun:

 import mod mybar = mod.Bar()

Sie würden eine hässliche AttributeError -Ausnahme erhalten.

Warum? Weil, wie hier berichtet, alle globalen Variablen des Moduls auf None gesetzt sind, wenn der Interpreter heruntergefahren wird. Als Ergebnis wurde im obigen Beispiel an dem Punkt, an dem __del__ aufgerufen wird, der Name foo bereits auf None gesetzt.

Eine Lösung für dieses etwas fortgeschrittenere Python-Programmierproblem wäre die Verwendung von atexit.register() stattdessen. Auf diese Weise werden Ihre registrierten Handler gestartet, wenn die Ausführung Ihres Programms beendet ist (dh wenn es normal beendet wird), bevor der Interpreter heruntergefahren wird.

Mit diesem Verständnis könnte ein Fix für den obigen mod.py -Code dann etwa so aussehen:

 import foo import atexit def cleanup(handle): foo.cleanup(handle) class Bar(object): def __init__(self): ... atexit.register(cleanup, self.myhandle)

Diese Implementierung stellt einen sauberen und zuverlässigen Weg bereit, jede benötigte Bereinigungsfunktion bei normaler Programmbeendigung aufzurufen. Offensichtlich liegt es an foo.cleanup , zu entscheiden, was mit dem Objekt geschehen soll, das an den Namen self.myhandle gebunden ist, aber Sie haben die Idee.

Einpacken

Python ist eine leistungsstarke und flexible Sprache mit vielen Mechanismen und Paradigmen, die die Produktivität erheblich verbessern können. Wie bei jedem Software-Tool oder jeder Sprache kann ein begrenztes Verständnis oder eine begrenzte Wertschätzung ihrer Fähigkeiten manchmal eher ein Hindernis als ein Vorteil sein und einen in dem sprichwörtlichen Zustand zurücklassen, „genug zu wissen, um gefährlich zu sein“.

Wenn Sie sich mit den wichtigsten Nuancen von Python vertraut machen, wie (aber keineswegs beschränkt auf) die mäßig fortgeschrittenen Programmierprobleme, die in diesem Artikel angesprochen werden, wird dies dazu beitragen, die Verwendung der Sprache zu optimieren und gleichzeitig einige ihrer häufigeren Fehler zu vermeiden.

Vielleicht möchten Sie auch in unserem Insider-Leitfaden für Python-Interviews nach Vorschlägen zu Interviewfragen suchen, die Ihnen dabei helfen können, Python-Experten zu identifizieren.

Wir hoffen, Sie fanden die Hinweise in diesem Artikel hilfreich und freuen uns über Ihr Feedback.