JavaRush /Курси /Модуль 1: Python Core /Стандартні помилки, частина 2

Стандартні помилки, частина 2

Модуль 1: Python Core
Рівень 11 , Лекція 7
Відкрита

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, це важливо розуміти в будь-якому випадку.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ