8.1 Непонимание правил области видимости Python
Область видимости в Python основана на так называемом правиле LEGB, которое является аббревиатурой:
-
Local(имена, назначенные любым способом внутри функции (defилиlambda), и не объявленные глобальными в этой функции); Enclosing(имена в локальной области действия любых статически включающих функций (defилиlambda), от внутреннего к внешнему);Global(имена, назначенные на верхнем уровне файла модуля, или путем выполненияglobalинструкции вdefвнутри файла);-
Built-in(имена, предварительно назначенные в модуле встроенных имен:open,range,SyntaxErrorи другие).
Кажется, достаточно просто, верно?
Однако есть некоторые тонкости в том, как это работает в Python, что подводит нас к сложной проблеме программирования на Python. Рассмотрим следующий пример:
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
В чем проблема?
Вышеуказанная ошибка возникает потому, что, когда вы присваиваете значение переменной в области видимости, Python автоматически считает ее локальной для этой области и скрывает любую переменную с аналогичным именем в любой вышестоящей области.
Таким образом, многие удивляются, когда получают UnboundLocalError в ранее работающем коде, когда он модифицируется путем добавления оператора присваивания где-нибудь в теле функции.
Эта особенность особенно сбивает разработчиков с толку при использовании списков. Рассмотрим следующий пример:
lst = [1, 2, 3]
def foo1():
lst.append(5) # Это работает нормально...
foo1()
print(lst)
[1, 2, 3, 5]
lst = [1, 2, 3]
def foo2():
lst += [5] # ... а вот это падает!
foo2()
Traceback (most recent call last):
File "
", line 1, in
File "
", line 2, in foo UnboundLocalError: local variable 'lst' referenced before assignment
Почему foo2 падает, в то время как foo1 работает нормально?
Ответ такой же, как в предыдущем примере, но, по распространенному мнению, здесь ситуация более тонкая. foo1 не применяет оператор присваивания к lst, тогда как foo2 — да. Помня, что lst += [5] на самом деле является просто сокращением для lst = lst + [5], мы видим, что мы пытаемся присвоить значение lst (поэтому Python предполагает, что он находится в локальной области видимости). Однако значение, которое мы хотим присвоить lst, основано на самом lst (опять же, теперь предполагается, что он находится в локальной области видимости), который еще не был определен. И мы получаем ошибку.
8.2 Изменение списка во время итерации по нему
Проблема в следующем куске кода должна быть достаточно очевидной:
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 "
", line 2, in
IndexError: list index out of range
Удаление элемента из списка или массива во время итерации по нему — это проблема Python, которая хорошо известна любому опытному разработчику программного обеспечения. Но, хотя приведенный выше пример может быть достаточно очевидным, даже опытные разработчики могут встать на эти грабли в гораздо более сложном коде.
К счастью, Python включает в себя ряд элегантных парадигм программирования, которые при правильном использовании могут привести к значительному упрощению и оптимизации кода. Дополнительным приятным следствием этого является то, что в более простом коде вероятность попасться на ошибку случайного удаления элемента списка во время итерации по нему значительно меньше.
Одна из таких парадигм — генераторы списков. Кроме того, понимание работы генераторов списков особенно полезно для избежания этой конкретной проблемы, как показано в этой альтернативной реализации приведенного выше кода, которая прекрасно работает:
odd = lambda x: bool(x % 2)
numbers = [n for n in range(10)]
numbers[:] = [n for n in numbers if not odd(n)] # просто отбираем новые элементы
print(numbers)
# [0, 2, 4, 6, 8]
Важно! Тут не происходит присваивания нового объекта списка. Использование numbers[:] — это групповое присваивание новых значений всем элементам списка.
8.3 Непонимание того, как Python связывает переменные в замыканиях
Рассмотрим следующий пример:
def create_multipliers():
return [lambda x: i * x for i in range(5)] # Возвращает список функций!
for multiplier in create_multipliers():
print(multiplier(2))
Вы можете ожидать следующий вывод:
0
2
4
6
8
Но на самом деле вы получите вот что:
8
8
8
8
8
Сюрприз!
Это происходит из-за поздней привязки в Python, которая заключается в том, что значения переменных, используемых в замыканиях, ищутся во время вызова внутренней функции.
Таким образом, в приведенном выше коде всякий раз, когда вызывается какая-либо из возвращаемых функций, значение i ищется в окружающей области видимости во время ее вызова (а к тому времени цикл уже завершился, поэтому i уже был присвоен конечный результат — значение 4).
Решение этой распространенной проблемы с Python будет таким:
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
Вуаля! Мы используем здесь аргументы по умолчанию для генерации анонимных функций для достижения желаемого поведения. Некоторые назвали бы это решение элегантным. Некоторые — тонким. Некоторые ненавидят подобные штуки. Но если вы разработчик Python, это важно понимать в любом случае.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ