8.1 Niezrozumienie zasad zasięgu w Pythonie
Zasięg w Pythonie opiera się na tak zwanej zasadzie LEGB, która jest skrótem:
-
Local
(nazwy przypisane w dowolny sposób wewnątrz funkcji (def
lublambda
), i nieogłoszone jako globalne w tej funkcji); Enclosing
(nazwy w lokalnym zasięgu dowolnych statycznie obejmujących funkcji (def
lublambda
), od wewnątrz na zewnątrz);Global
(nazwy przypisane na najwyższym poziomie pliku modułu lub przez wykonanie instrukcjiglobal
wdef
wewnątrz pliku);-
Built-in
(nazwy wcześniej przypisane w module wbudowanych nazw:open
,range
,SyntaxError
, i inne).
Brzmi prosto, prawda?
Niemniej jednak istnieją pewne niuanse w tym, jak to działa w Pythonie, co prowadzi nas do złożonego problemu programowania w Pythonie. Przyjrzyjmy się poniższemu przykładowi:
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
Gdzie jest problem?
Powyższy błąd pojawia się, ponieważ, kiedy przypisujesz wartość do zmiennej w zasięgu
, Python automatycznie uznaje ją za lokalną
dla tego zasięgu i ukrywa wszelkie zmienne o tej samej nazwie w jakimkolwiek nadrzędnym zasięgu.
Wiele osób zdziwi się, kiedy dostają UnboundLocalError
w kodzie, który wcześniej działał, kiedy zostanie zmodyfikowany poprzez dodanie operatora przypisania gdziekolwiek w ciele funkcji.
Ta cecha szczególnie myli programistów przy użyciu list. Rozważmy poniższy przykład:
lst = [1, 2, 3]
def foo1():
lst.append(5) # To działa normalnie...
foo1()
print(lst)
[1, 2, 3, 5]
lst = [1, 2, 3]
def foo2():
lst += [5] # ... a to już się zawiesza!
foo2()
Traceback (most recent call last):
File "
", line 1, in
File "
", line 2, in foo UnboundLocalError: local variable 'lst' referenced before assignment
Dlaczego foo2
się zawiesza, a foo1
działa normalnie?
Odpowiedź jest taka sama, jak w poprzednim przykładzie, ale powszechnie uważa się, że sytuacja tutaj jest bardziej subtelna. foo1
nie stosuje operatora przypisania do lst
, podczas gdy foo2
— tak. Pamiętając, że lst += [5]
to w istocie tylko skrót dla lst = lst + [5]
, widzimy, że próbujemy przypisać wartość do lst
(więc Python przypuszcza, że znajduje się w lokalnym zasięgu). Jednak wartość, którą chcemy przypisać do lst
, opiera się na samym lst
(znowu, teraz przypuszcza się, że znajduje się w lokalnym zasięgu), który jeszcze nie został zdefiniowany. I mamy błąd.
8.2 Zmiana listy podczas iteracji po niej
Problem w tym kawałku kodu powinien być dość oczywisty:
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] # ZŁE: Usuwanie elementu z listy podczas iteracji po niej
Traceback (most recent call last):
File "
", line 2, in
IndexError: list index out of range
Usuwanie elementu z listy lub tablicy podczas iteracji po niej to problem w Pythonie, który jest dobrze znany wszystkim doświadczonym programistom. Ale choć powyższy przykład może być wystarczająco oczywisty, nawet doświadczeni programiści mogą wpaść na te pułapki w dużo bardziej skomplikowanym kodzie.
Na szczęście Python zawiera szereg eleganckich paradygmatów programowania, które użyte poprawnie mogą prowadzić do znacznego uproszczenia i optymalizacji kodu. Dodatkowym przyjemnym skutkiem tego jest to, że w bardziej uproszczonym kodzie szansa na napotkanie błędu przypadkowego usunięcia elementu listy podczas iteracji jest znacznie mniejsza.
Jednym z takich paradygmatów są generatory list. Co więcej, zrozumienie działania generatorów list jest szczególnie przydatne do uniknięcia tego konkretnego problemu, jak pokazano w tej alternatywnej realizacji powyższego kodu, która działa idealnie:
odd = lambda x: bool(x % 2)
numbers = [n for n in range(10)]
numbers[:] = [n for n in numbers if not odd(n)] # po prostu wybieramy nowe elementy
print(numbers)
# [0, 2, 4, 6, 8]
Ważne!
Tutaj nie ma przypisania nowego obiektu listy. Użycie numbers[:]
— to zbiorcze przypisanie nowych wartości do wszystkich elementów listy.
8.3 Niezrozumienie, jak Python wiąże zmienne w zamknięciach
Przyjrzyjmy się poniższemu przykładowi:
def create_multipliers():
return [lambda x: i * x for i in range(5)] #Zwraca listę funkcji!
for multiplier in create_multipliers():
print(multiplier(2))
Możesz spodziewać się następującego wyjścia:
0
2
4
6
8
Ale w rzeczywistości otrzymasz:
8
8
8
8
8
Niespodzianka!
To się dzieje z powodu późnego wiązania w Pythonie, które oznacza, że wartości zmiennych używanych w zamknięciach są wyszukiwane podczas wywołania wewnętrznej funkcji.
Zatem w powyższym kodzie za każdym razem, gdy któraś z zwracanych funkcji jest wywoływana, wartość i
jest wyszukiwana w otaczającym zasięgu podczas jej wywołania (w tym momencie pętla już się zakończyła, więc i
zostało przypisane do ostatecznego wyniku — wartości 4).
Rozwiązanie tego powszechnego problemu w Pythonie wygląda następująco:
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à! Używamy tutaj domyślnych argumentów do generowania funkcji anonimowych, aby osiągnąć pożądane zachowanie. Niektórzy nazwaliby to rozwiązanie eleganckim. Niektórzy — subtelnym. Niektórzy nienawidzą takich sztuczek. Ale jeśli jesteś programistą Pythona, to ważne, aby to zrozumieć w każdym przypadku.
GO TO FULL VERSION