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: 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, это важно понимать в любом случае.

2
Задача
Модуль 1: Python Core, 11 уровень, 7 лекция
Недоступна
Исправляем глобальные переменные.
Исправляем глобальные переменные.
2
Задача
Модуль 1: Python Core, 11 уровень, 7 лекция
Недоступна
Исправляем замыкания.
Исправляем замыкания.
Комментарии (13)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
11 июня 2025
Мне вот кажется, что разбор частых ошибок нужно было включать не отдельным разделом, а как лекцию в конце каждого раздела. Прошли к примеру списки и в последней лекции разобрали частые ошибки которые возникают при работе со списками, аналогично с классами и т.д.
SWK Уровень 26
11 апреля 2025
Люди добрые! Слабоумные авторы думают, что вы поняли пример про вот эту лямбду:

return [lambda x, i = i : i * x for i in range(5)]
при том, что как такие лямбды устроены они не поясняли никогда. Не поленитесь, пожалуйста, напишите им, часть того, что вы о них думаете!
Дмитрий Уровень 27
7 мая 2025
Ну тут, вроде, список из 5 лямбда-функций получился, и мы их по-очереди итерируем. Но проблема, что i, на которую мы ссылаемся в области видимости функции не определена, поэтому пайтон лезет в область видимости итератора, где i определена и равна 4, потому что функции уже сгенерированы и положены в список. Поэтому разумно сохранить в каждой лямбда-функции то значение i, которое было в момент её генерации, поэтому и используется это странное присваивание. Просто не надо было использовать внутри лямбда-функции переменную с таким же именем - это сбивает с толку. Вот так более корректно:

return [lambda x, y=i: y*x for i in range(5)]
SWK Уровень 26
11 апреля 2025

numbers = [n for n in range(10)]
numbers[:] = [n for n in numbers if not odd(n)]
"""...Тут не происходит присваивания нового объекта списка. 
Использование numbers[:] — это групповое присваивание новых значений всем элементам списка. """
# [0, 2, 4, 6, 8]
Ни на что не намекаю, но слева в numbers[:] 10 элементов (их туда только что положили, из range(10)), а справа - 5. Если происходит "групповое присваивание", то элементов таки должно остаться 10. А остаётся 5. Т.е., именно, что присваивание нового объекта, списка из пяти чётных чисел, туда, где был список из 10ти.
Slevin Уровень 64
7 июля 2025
Это именно тот же объект, что можно проверить по вызову print(id(numbers)) до и после присваивания. По сути, список сперва очищается - а затем переприсваивается заново.
_den Уровень 52
6 апреля 2025
[:] = - это способ перезаписать содержимое списка "на месте", не создавая новый объект. Удобно, когда важно сохранить ссылку на список (например, передали его в функцию или он используется в нескольких местах).
SWK Уровень 26
11 апреля 2025
Думаешь, что-то пояснил? Увы! Т.к., только ты знаешь, как кой смысл только ты вкладываешь в данном случае в выражение 'перезаписать содержимое списка "на месте", не создавая новый объект'.
_den Уровень 52
11 апреля 2025
постарался расписать, но только я знаю какой смысл вкладываю)

odd = lambda x: bool(x % 2)

numbers1 = [n for n in range(10)]
numbers2 = numbers1
print(f'До изменения: numbers1={numbers1}, numbers2={numbers2}')
print(f'id(numbers1) = {id(numbers1)}, id(numbers2) = {id(numbers2)}, numbers1==numbers2? {numbers1==numbers2}') # обе переменные ссылаются на один список

# делаем обычное присваивание значения
numbers1 = [n for n in numbers1 if not odd(n)] #пишем результат в НОВЫЙ список
print(f'После изменения: numbers1={numbers1}, numbers2={numbers2}') # списки разные, изменился только первый
print(f'id(numbers1) = {id(numbers1)}, id(numbers2) = {id(numbers2)}, numbers1==numbers2? {numbers1==numbers2}') # id отличанются, у первого списка изменился

print('-'*100)
# повторим для [:]
numbers1 = [n for n in range(10)]
numbers2 = numbers1
print(f'До изменения: numbers1={numbers1}, numbers2={numbers2}')
print(f'id(numbers1) = {id(numbers1)}, id(numbers2) = {id(numbers2)}, numbers1==numbers2? {numbers1==numbers2}') # обе переменные ссылаются на один список

# используем срез [:]
numbers1[:] = [n for n in numbers1 if not odd(n)] # пишем результат в ИСХОДНЫЙ список
print(f'После изменения: numbers1={numbers1}, numbers2={numbers2}') # списки изменились, но совпадают
print(f'id(numbers1) = {id(numbers1)}, id(numbers2) = {id(numbers2)}, numbers1==numbers2? {numbers1==numbers2}') # id не изменился
_den Уровень 52
11 апреля 2025
результат выполнения:

До изменения: numbers1=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], numbers2=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
id(numbers1) = 131897687788352, id(numbers2) = 131897687788352, numbers1==numbers2? True
После изменения: numbers1=[0, 2, 4, 6, 8], numbers2=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
id(numbers1) = 131897677102592, id(numbers2) = 131897687788352, numbers1==numbers2? False
----------------------------------------------------------------------------------------------------
До изменения: numbers1=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], numbers2=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
id(numbers1) = 131897677102656, id(numbers2) = 131897677102656, numbers1==numbers2? True
После изменения: numbers1=[0, 2, 4, 6, 8], numbers2=[0, 2, 4, 6, 8]
id(numbers1) = 131897677102656, id(numbers2) = 131897677102656, numbers1==numbers2? True
когда присваиваем значения с [:], то ссылка на объект не меняется
SWK Уровень 26
14 апреля 2025
Блин! Так это выглядит ещё контринтуитивнее, чтобы не сказать противоестественнее. С какого перепуга нечто, называемое срезом первого массива, переопределяет второй?
Дмитрий Уровень 27
7 мая 2025
Наверное тут надо код самого пайтона смотреть, и метод, реализующий получение данных через слайс в классе list. Метод там дуракозащищённый, и при выходе за индексы, например, не вызывает ошибку. А при передаче данных без указания границ, как видно, просто трёт всё содержимое списка и переписывает полученными данными, не меняя id объекта, т.е. в самом объекте. Т.е. если случайно в гигабайтный спикок передать значение без указания границ, то можно весь его потереть. По-крайней мере, так выглядит:

def some(lst):
    print(f'obj ID: {id(lst)}')
    for item in lst: print(item)
a = [0,1,2]
b = [5,6,7]
c = ['a','b','c']
some(c)
print('-'*30)
c[2:9] = ['d','e']
some(c)
print('-'*30)
c[:] = a + b
some(c)
obj ID: 134093350412928 a b c ------------------------------ obj ID: 134093350412928 a b d e ------------------------------ obj ID: 134093350412928 0 1 2 5 6 7
Ivan Уровень 59
13 мая 2025
> А при передаче данных без указания границ, как видно, просто трёт всё содержимое списка Я не знаю, как конкретно устроены листы в пайтоне, но в других языках программирования внутре листа лежит массив, длина которого не обязана равняться длине листа. Длина листа хранится в отдельной переменной. Поэтому при переназначении в ту же область памяти нового листа меньшей длины не надо ничего тереть. Просто первые n элементов заполняются нужными значениями, и меняется значение переменной, отвечающей за длину. Весь остальной массив может продолжать хранить старые значения и перезаписывать их по мере необходимости, если лист растёт.
Slevin Уровень 64
7 июля 2025
в Питоне переменная является ссылкой на объект, а не самим объектом. Потому переписывая сам объект - изменится и вывод других переменных, которые на него ссылались.