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: Видалення елемента зі списку під час ітерації по ньому
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, це важливо розуміти в будь-якому випадку.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ