7.1 Неправильное использование выражений в качестве значений по умолчанию для аргументов функций
Вы уже многому научились, давайте разберем самые стандартные ошибки новичков – лучше учиться на чужих ошибках, чем на своих – не так ли?
Python позволяет указывать, что у функции могут быть необязательные аргументы, путем задания для них значения по умолчанию. Это, конечно, очень удобная особенность языка, но может привести к неприятным последствиям, если тип такого значения будет изменяемым. Например, рассмотрим следующее определение функции:
def foo(bar=[]): # bar - это необязательный аргумент
# и по умолчанию равен пустому списку.
bar.append("baz") # эта строка может стать проблемой...
return bar
Распространенная ошибка в данном случае — это думать, что значение необязательного аргумента будет устанавливаться в значение по умолчанию каждый раз, как функция будет вызываться без значения для этого аргумента.
В приведенном выше коде, например, можно предположить, что повторно вызывая функцию foo()
(то есть без указания значения для аргумента bar
), она всегда будет возвращать ["baz"]
, поскольку предполагается, что каждый раз, когда вызывается foo()
(без указания аргумента bar
), bar
устанавливается в []
(т. е. новый пустой список).
Но давайте посмотрим, что же будет происходить на самом деле:
result = foo()
print(result) # ["baz"]
result = foo()
print(result) # ["baz", "baz"]
result = foo()
print(result) # ["baz", "baz", "baz"]
Почему же функция продолжает добавлять значение baz
к существующему списку каждый раз, когда вызывается foo()
, вместо того чтобы каждый раз создавать новый список?
Ответом на данный вопрос будет более глубокое понимание того, что творится у Python «под капотом». А именно: значение по умолчанию для функции инициализируется только один раз, во время определения функции.
Таким образом, аргумент bar
инициализируется по умолчанию (т. е. пустым списком) только тогда, когда foo()
определен впервые, но последующие вызовы foo()
(т. е. без указания аргумента bar
) продолжат использовать тот же список, который был создан для аргумента bar
в момент первого определения функции.
Для справки, распространенным «обходным путем» для этой ошибки является следующее определение:
def foo(bar=None):
if bar is None:
bar = []
bar.append("baz")
return bar
result = foo()
print(result) # ["baz"]
result = foo()
print(result) # ["baz"]
result = foo()
print(result) # ["baz"]
7.2 Неправильное использование переменных класса
Рассмотрим следующий пример:
class A(object):
x = 1
class B(A):
pass
class C(A):
pass
print(A.x, B.x, C.x)
# 1 1 1
Вроде все в порядке.
Теперь давайте присвоим значение полю x
класса B
:
B.x = 2
print(A.x, B.x, C.x)
# 1 2 1
Все как и ожидалось.
А теперь поменяем переменную класса A
:
A.x = 3
print(A.x, B.x, C.x)
# 3 2 3
Странно, мы же только изменили A.x
. Почему же C.x
тоже изменилось?
В Python переменные класса обрабатываются как словари и следуют тому, что часто называют Порядком разрешения методов (MRO). Таким образом, в приведенном выше коде, поскольку атрибут x
не найден в классе C
, он будет найден в его базовых классах (только A
в приведенном выше примере, хотя Python поддерживает множественное наследование).
Другими словами, C
не имеет своего собственного свойства x
, независимого от A
. Таким образом, ссылки на C.x
фактически являются ссылками на A.x
. Это может вызвать проблемы, если не учитывать такие случаи должным образом. При изучении Python обратите особое внимание на атрибуты класса и работу с ними.
7.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
Проблема здесь заключается в том, что выражение except
не принимает список исключений, указанных таким образом. В результате в приведенном выше коде исключение IndexError
не перехватывается выражением except
; вместо этого исключение заканчивается привязкой к параметру с именем IndexError
.
Важно!
Такой код вы можете встретить в примерах в интернете, так как он использовался в Python 2.x.
Правильный способ перехвата нескольких исключений с помощью выражения except
— указать первый параметр в виде кортежа, содержащего все исключения, которые нужно перехватить. Кроме того, для лучшей читаемости вы можете использовать ключевое слово as
, например так:
try:
l = ["a", "b"]
int(l[2])
except (ValueError, IndexError) as e:
pass
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ