Глючный код Python: 10 самых распространенных ошибок, которые допускают разработчики Python

Опубликовано: 2022-03-11

О Питоне

Python — это интерпретируемый объектно-ориентированный язык программирования высокого уровня с динамической семантикой. Его высокоуровневые встроенные структуры данных в сочетании с динамической типизацией и динамической привязкой делают его очень привлекательным для быстрой разработки приложений, а также для использования в качестве языка сценариев или связующего языка для соединения существующих компонентов или служб. Python поддерживает модули и пакеты, тем самым поощряя модульность программы и повторное использование кода.

Об этой статье

Простой и легкий для изучения синтаксис Python может ввести в заблуждение разработчиков Python, особенно тех, кто не знаком с этим языком, и заставить их упустить некоторые из его тонкостей и недооценить мощь разнообразного языка Python.

Имея это в виду, в этой статье представлен список «10 лучших» довольно тонких, трудноуловимых ошибок, которые могут укусить даже некоторых более продвинутых разработчиков Python.

(Примечание: эта статья предназначена для более продвинутой аудитории, чем «Распространенные ошибки программистов на Python», которая больше ориентирована на тех, кто не знаком с языком.)

Распространенная ошибка №1: Неправильное использование выражений в качестве значений по умолчанию для аргументов функций

Python позволяет указать, что аргумент функции является необязательным , предоставив для него значение по умолчанию . Хотя это отличная особенность языка, она может привести к некоторой путанице, если значением по умолчанию является mutable . Например, рассмотрим это определение функции Python:

 >>> def foo(bar=[]): # bar is optional and defaults to [] if not specified ... bar.append("baz") # but this line could be problematic, as we'll see... ... return bar

Распространенной ошибкой является мнение, что необязательный аргумент будет устанавливаться в указанное выражение по умолчанию каждый раз , когда функция вызывается без указания значения для дополнительного аргумента. В приведенном выше коде, например, можно было бы ожидать, что повторный вызов foo() (т. е. без указания аргумента bar ) всегда будет возвращать 'baz' , поскольку предполагается, что каждый раз , когда вызывается foo() () (без аргумента bar указанный аргумент) bar устанавливается в [] (т. е. новый пустой список).

Но давайте посмотрим, что на самом деле происходит, когда вы это делаете:

 >>> foo() ["baz"] >>> foo() ["baz", "baz"] >>> foo() ["baz", "baz", "baz"]

Хм? Почему он продолжал добавлять значение по умолчанию "baz" к существующему списку каждый раз при вызове foo() , а не создавать каждый раз новый список?

Более продвинутый ответ программирования на Python заключается в том, что значение по умолчанию для аргумента функции оценивается только один раз, во время определения функции. Таким образом, аргумент bar инициализируется своим значением по умолчанию (т. е. пустым списком) только тогда, когда foo() определена впервые, но затем вызовы foo() (т. е. без указания аргумента bar ) будут продолжать использовать тот же список для какой bar был изначально инициализирован.

К вашему сведению, общий обходной путь для этого выглядит следующим образом:

 >>> def foo(bar=None): ... if bar is None: # or if not bar: ... bar = [] ... bar.append("baz") ... return bar ... >>> foo() ["baz"] >>> foo() ["baz"] >>> foo() ["baz"]

Распространенная ошибка № 2: неправильное использование переменных класса

Рассмотрим следующий пример:

 >>> class A(object): ... x = 1 ... >>> class B(A): ... pass ... >>> class C(A): ... pass ... >>> print Ax, Bx, Cx 1 1 1

Имеет смысл.

 >>> Bx = 2 >>> print Ax, Bx, Cx 1 2 1

Ага, опять как и ожидалось.

 >>> Ax = 3 >>> print Ax, Bx, Cx 3 2 3

Что за $%#!& ?? Мы только изменили Ax . Почему Cx тоже изменился?

В Python переменные класса внутренне обрабатываются как словари и следуют тому, что часто называют порядком разрешения методов (MRO). Таким образом, в приведенном выше коде, поскольку атрибут x не найден в классе C , он будет искаться в его базовых классах (только A в приведенном выше примере, хотя Python поддерживает множественное наследование). Другими словами, C не имеет собственного свойства x , независимого от A Таким образом, ссылки на Cx фактически являются ссылками на Ax . Это вызывает проблему Python, если она не обрабатывается должным образом. Узнайте больше об атрибутах класса в Python.

Распространенная ошибка №3: ​​неправильное указание параметров для блока исключений

Предположим, у вас есть следующий код:

 >>> try: ... l = ["a", "b"] ... int(l[2]) ... except ValueError, IndexError: # To catch both exceptions, right? ... pass ... Traceback (most recent call last): File "<stdin>", line 3, in <module> IndexError: list index out of range

Проблема здесь в том, что оператор exclude не принимает список except , указанных таким образом. Скорее, в Python 2.x синтаксис, except Exception, e используется для привязки исключения к указанному необязательному второму параметру (в данном случае e ), чтобы сделать его доступным для дальнейшей проверки. В результате в приведенном выше коде except IndexError не перехватывается оператором exclude; вместо этого исключение оказывается привязанным к параметру с именем IndexError .

Правильный способ перехвата нескольких except в операторе exclude — указать первый параметр как кортеж, содержащий все исключения, которые необходимо перехватить. Кроме того, для максимальной переносимости используйте ключевое слово as , поскольку этот синтаксис поддерживается как Python 2, так и Python 3:

 >>> try: ... l = ["a", "b"] ... int(l[2]) ... except (ValueError, IndexError) as e: ... pass ... >>>

Распространенная ошибка № 4: непонимание правил области видимости Python

Разрешение области действия Python основано на так называемом правиле LEGB, которое является сокращением от Local, Enclosing , Global , B uilt -in. Кажется достаточно простым, не так ли? Ну, на самом деле, есть некоторые тонкости в том, как это работает в 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) # This works ok... ... >>> foo1() >>> lst [1, 2, 3, 5] >>> lst = [1, 2, 3] >>> def foo2(): ... lst += [5] # ... but this bombs! ... >>> foo2() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", 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 (опять же, теперь предполагается, что оно находится в локальной области видимости), которое еще не определено. Бум.

Распространенная ошибка № 5: изменение списка во время его повторения

Проблема со следующим кодом должна быть довольно очевидной:

 >>> 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 "<stdin>", line 2, in <module> 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)] # ahh, the beauty of it all >>> numbers [0, 2, 4, 6, 8]

Распространенная ошибка № 6: путаница в том, как 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, это важно понимать в любом случае.

Распространенная ошибка № 7: создание круговых зависимостей модулей

Допустим, у вас есть два файла, a.py и b.py , каждый из которых импортирует другой, как показано ниже:

В a.py :

 import b def f(): return bx print f()

И в b.py :

 import a x = 1 def g(): print af()

Во-первых, давайте попробуем импортировать a.py :

 >>> import a 1

Работал просто отлично. Возможно, это вас удивляет. В конце концов, у нас есть циклический импорт, который, по-видимому, должен быть проблемой, не так ли?

Ответ заключается в том, что само по себе наличие циклического импорта не является проблемой в Python. Если модуль уже был импортирован, Python достаточно умен, чтобы не пытаться повторно импортировать его. Однако, в зависимости от точки, в которой каждый модуль пытается получить доступ к функциям или переменным, определенным в другом, вы действительно можете столкнуться с проблемами.

Итак, возвращаясь к нашему примеру, когда мы импортировали a.py , у него не было проблем с импортом b.py , поскольку b.py не требует, чтобы что-либо определялось из a.py во время импорта . Единственная ссылка в b.py на a — это вызов af() . Но этот вызов находится в g() , и ничто в a.py или b.py вызывает g() . Так что жизнь удалась.

Но что произойдет, если мы попытаемся импортировать b.py (без предварительного импорта a.py ):

 >>> import b Traceback (most recent call last): File "<stdin>", line 1, in <module> File "b.py", line 1, in <module> import a File "a.py", line 6, in <module> print f() File "a.py", line 4, in f return bx AttributeError: 'module' object has no attribute 'x'

О-о. Это не хорошо! Проблема здесь в том, что в процессе импорта b.py он пытается импортировать a.py , который, в свою очередь, вызывает f() , которая пытается получить доступ к bx . Но bx еще не определен. Отсюда и исключение AttributeError .

По крайней мере, одно решение этого довольно тривиально. Просто измените b.py , чтобы импортировать a.py в g() :

 x = 1 def g(): import a # This will be evaluated only when g() is called print af()

Нет, когда мы его импортируем, все нормально:

 >>> import b >>> bg() 1 # Printed a first time since module 'a' calls 'print f()' at the end 1 # Printed a second time, this one is our call to 'g'

Распространенная ошибка № 8: Конфликт имен с модулями стандартной библиотеки Python

Одна из прелестей Python — это множество библиотечных модулей, которые поставляются «из коробки». Но в результате, если вы не избегаете этого сознательно, не так сложно столкнуться с конфликтом имен между именем одного из ваших модулей и модулем с таким же именем в стандартной библиотеке, поставляемой с Python (например, , в вашем коде может быть модуль с именем email.py , который будет конфликтовать с одноименным модулем стандартной библиотеки).

Это может привести к грубым проблемам, таким как импорт другой библиотеки, которая, в свою очередь, пытается импортировать версию модуля из стандартной библиотеки Python, но, поскольку у вас есть модуль с тем же именем, другой пакет по ошибке импортирует вашу версию вместо той, что внутри. Стандартная библиотека Python. Вот где случаются плохие ошибки Python.

Поэтому следует проявлять осторожность, чтобы не использовать те же имена, что и в модулях стандартной библиотеки Python. Вам намного проще изменить имя модуля в вашем пакете, чем подать предложение по улучшению Python (PEP), чтобы запросить изменение имени вверх по течению и попытаться получить его одобрение.

Распространенная ошибка № 9: Неспособность устранить различия между Python 2 и Python 3

Рассмотрим следующий файл foo.py :

 import sys def bar(i): if i == 1: raise KeyError(1) if i == 2: raise ValueError(2) def bad(): e = None try: bar(int(sys.argv[1])) except KeyError as e: print('key error') except ValueError as e: print('value error') print(e) bad()

На Python 2 это работает нормально:

 $ python foo.py 1 key error 1 $ python foo.py 2 value error 2

А теперь давайте попробуем на Python 3:

 $ python3 foo.py 1 key error Traceback (most recent call last): File "foo.py", line 19, in <module> bad() File "foo.py", line 17, in bad print(e) UnboundLocalError: local variable 'e' referenced before assignment

Что здесь только что произошло? «Проблема» заключается в том, что в Python 3 объект исключения недоступен за пределами области блока except . (Причина этого в том, что в противном случае цикл ссылок со фреймом стека будет храниться в памяти до тех пор, пока сборщик мусора не запустится и не удалит ссылки из памяти. Более подробная техническая информация об этом доступна здесь).

Один из способов избежать этой проблемы — поддерживать ссылку на объект исключения за пределами области блока except , чтобы он оставался доступным. Вот версия предыдущего примера, в которой используется этот метод, в результате чего получается код, совместимый как с Python 2, так и с Python 3:

 import sys def bar(i): if i == 1: raise KeyError(1) if i == 2: raise ValueError(2) def good(): exception = None try: bar(int(sys.argv[1])) except KeyError as e: exception = e print('key error') except ValueError as e: exception = e print('value error') print(exception) good()

Запуск этого на Py3k:

 $ python3 foo.py 1 key error 1 $ python3 foo.py 2 value error 2

Ура!

(Кстати, в нашем Руководстве по найму Python обсуждается ряд других важных отличий, о которых следует помнить при переносе кода с Python 2 на Python 3.)

Распространенная ошибка № 10: Неправильное использование метода __del__

Допустим, у вас было это в файле с именем mod.py :

 import foo class Bar(object): ... def __del__(self): foo.cleanup(self.myhandle)

Затем вы попытались сделать это из another_mod.py :

 import mod mybar = mod.Bar()

Вы получите уродливое исключение AttributeError .

Почему? Потому что, как сообщается здесь, когда интерпретатор выключается, все глобальные переменные модуля устанавливаются в None . В результате в приведенном выше примере в момент __del__ для имени foo уже было установлено значение None .

Решением этой несколько более сложной проблемы программирования на Python было бы использование atexit.register() вместо этого. Таким образом, когда ваша программа завершает выполнение (то есть при обычном выходе), ваши зарегистрированные обработчики запускаются до закрытия интерпретатора.

При таком понимании исправление для приведенного выше кода mod.py может выглядеть примерно так:

 import foo import atexit def cleanup(handle): foo.cleanup(handle) class Bar(object): def __init__(self): ... atexit.register(cleanup, self.myhandle)

Эта реализация обеспечивает чистый и надежный способ вызова любых необходимых функций очистки после нормального завершения программы. Очевидно, foo.cleanup должен решить, что делать с объектом, привязанным к имени self.myhandle , но вы поняли идею.

Заворачивать

Python — это мощный и гибкий язык со множеством механизмов и парадигм, которые могут значительно повысить производительность. Однако, как и в случае с любым программным инструментом или языком, ограниченное понимание или оценка его возможностей иногда может быть скорее препятствием, чем преимуществом, оставляя человека в пресловутом состоянии «знания достаточно, чтобы быть опасным».

Знакомство с ключевыми нюансами Python, такими как (но не ограничиваясь ими) умеренно сложные проблемы программирования, поднятые в этой статье, поможет оптимизировать использование языка, избегая при этом некоторых наиболее распространенных ошибок.

Вы также можете ознакомиться с нашим Инсайдерским руководством по проведению собеседований на Python, чтобы узнать, какие вопросы для собеседования помогут выявить экспертов по Python.

Мы надеемся, что советы в этой статье были вам полезны, и будем рады вашим отзывам.