8.1 Missverständnis der Python Sichtbarkeitsregeln
Die Sichtbarkeit in Python basiert auf der sogenannten LEGB-Regel, die eine Abkürzung ist:
-
Local(Namen, die innerhalb einer Funktion (defoderlambda) auf jede Weise zugewiesen werden, und nicht in dieser Funktion als global deklariert sind); Enclosing(Namen im lokalen Gültigkeitsbereich von beliebigen statisch einschließenden Funktionen (defoderlambda), von innen nach außen);Global(Namen, die auf oberster Ebene in einer Moduldatei zugewiesen werden, oder durch Ausführen einerglobal-Anweisung indefinnerhalb der Datei);-
Built-in(Namen, die zuvor im Modul der eingebauten Namen zugewiesen wurden:open,range,SyntaxError, und andere).
Das klingt ziemlich einfach, oder?
Es gibt jedoch einige Feinheiten in der Funktionsweise in Python, die uns zu einem kniffligen Problem beim Programmieren in Python führen. Betrachten wir das folgende Beispiel:
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
Wo ist das Problem?
Der obige Fehler tritt auf, weil wenn du einer Variablen im Gültigkeitsbereich einen Wert zuweist, Python automatisch davon ausgeht, dass sie lokal für diesen Bereich ist und jede Variable mit demselben Namen in einem darüberliegenden Bereich verdeckt.
Daher sind viele überrascht, wenn sie einen UnboundLocalError in zuvor funktionierendem Code erhalten, wenn dieser durch das Hinzufügen einer Zuweisungsanweisung irgendwo im Funktionskörper geändert wird.
Diese Besonderheit verwirrt Entwickler besonders beim Arbeiten mit Listen. Betrachten wir das folgende Beispiel:
lst = [1, 2, 3]
def foo1():
lst.append(5) # Das funktioniert problemlos...
foo1()
print(lst)
[1, 2, 3, 5]
lst = [1, 2, 3]
def foo2():
lst += [5] # ... aber das hier stürzt ab!
foo2()
Traceback (most recent call last):
File "
", line 1, in
File "
", line 2, in foo UnboundLocalError: local variable 'lst' referenced before assignment
Warum stürzt foo2 ab, während foo1 problemlos funktioniert?
Die Antwort ist die gleiche wie im vorherigen Beispiel, aber, weit verbreitet, hier ist die Situation subtiler. foo1 verwendet keine Zuweisungsanweisung an lst, während foo2 dies tut. Denken wir daran, dass lst += [5] tatsächlich nur eine Abkürzung für lst = lst + [5] ist, sehen wir, dass wir versuchen, lst einen Wert zuzuweisen (daher nimmt Python an, dass es sich im lokalen Gültigkeitsbereich befindet). Der Wert, den wir lst zuweisen möchten, basiert jedoch auf lst selbst (wiederum wird angenommen, dass es sich im lokalen Gültigkeitsbereich befindet), das noch nicht definiert wurde. Und wir erhalten einen Fehler.
8.2 Änderung einer Liste während der Iteration
Das Problem im folgenden Codeabschnitt 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] # SCHLECHT: Element aus einer Liste löschen, während über sie iteriert wird
Traceback (most recent call last):
File "
", line 2, in
IndexError: list index out of range
Das Löschen eines Elements aus einer Liste oder einem Array während der Iteration darüber ist ein bekanntes Problem in Python, das jedem erfahrenen Softwareentwickler bekannt ist. Aber obwohl das obige Beispiel klar genug sein mag, treten selbst erfahrene Entwickler auf dieses Problem bei viel komplexerem Code.
Glücklicherweise bietet Python eine Reihe eleganter Programmierparadigmen, die, wenn sie korrekt eingesetzt werden, zu einer erheblichen Vereinfachung und Optimierung des Codes führen können. Ein angenehmer Nebeneffekt ist, dass bei einfacheren Code die Wahrscheinlichkeit, auf den Fehler des versehentlichen Löschens eines Listenelements während der Iteration darüber zu stolpern, erheblich geringer ist.
Eine dieser Paradigmen sind List Comprehensions. Zusätzlich ist das Verständnis von List Comprehensions besonders nützlich, um dieses spezifische Problem zu vermeiden, wie diese alternative Implementierung des obigen Codes zeigt, die einwandfrei funktioniert:
odd = lambda x: bool(x % 2)
numbers = [n for n in range(10)]
numbers[:] = [n for n in numbers if not odd(n)] # wir wählen einfach neue Elemente aus
print(numbers)
# [0, 2, 4, 6, 8]
Wichtig! Hier findet keine Zuweisung eines neuen Listenobjekts statt. Die Verwendung von numbers[:] ist eine Gruppenzuweisung neuer Werte für alle Elemente der Liste.
8.3 Missverständnis, wie Python Variablen in Closures bindet
Betrachten wir das folgende Beispiel:
def create_multipliers():
return [lambda x: i * x for i in range(5)] #Listet Funktionen auf!
for multiplier in create_multipliers():
print(multiplier(2))
Du könntest den folgenden Output erwarten:
0
2
4
6
8
Aber tatsächlich bekommst du das hier:
8
8
8
8
8
Überraschung!
Das geschieht aufgrund der späten Bindung in Python, was bedeutet, dass die Werte der Variablen, die in Closures verwendet werden, zur Laufzeit der inneren Funktion nachgeschlagen werden.
Daher wird im obigen Code, wann immer eine der zurückgegebenen Funktionen aufgerufen wird, der Wert von i im umgebenden Gültigkeitsbereich zum Zeitpunkt seines Aufrufs nachgeschlagen (und zu diesem Zeitpunkt wurde die Schleife bereits abgeschlossen, sodass i bereits mit dem Endergebnis - Wert 4 - zugewiesen wurde).
Die Lösung für dieses verbreitete Problem in Python wäre folgendes:
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
Voilà! Wir verwenden hier Standardargumente, um anonyme Funktionen zu generieren, um das gewünschte Verhalten zu erzielen. Einige würden diese Lösung elegant nennen. Andere subtil. Einige hassen solche Tricks. Aber wenn du ein Python-Entwickler bist, ist es wichtig, dies in jedem Fall zu verstehen.
GO TO FULL VERSION