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: # Щоб перехопити обидва винятки, так?
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
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ