Содержание страницы
Идиомы и анти-идиомы в Python¶Idioms and Anti-Idioms in Python
- Автор
Moshe Zadka
Этот документ находится в общественном достоянии.
Аннотация
Этот документ можно рассматривать как дополнение к руководству. В нём показано, как использовать Python и, что ещё важнее, как не использовать Python.
Конструкции языка, которые не следует использовать¶Language Constructs You Should Not Use
Хотя в Python относительно мало «подводных камней» по сравнению с другими языками, в нём всё ещё есть конструкции, которые полезны только в крайних случаях или откровенно опасны.
from module import *¶
Внутри определений функций¶Inside Function Definitions
from module import * недопустима внутри определений функций. Хотя многие версии Python не проверяют эту недопустимость, от этого она не становится более допустимой – не более, чем наличие умного адвоката делает человека невиновным. Никогда не используйте её так. Даже в версиях, где она принималась, это замедляло выполнение функции, потому что компилятор не мог быть уверен, какие имена локальные, а какие глобальные. В Python 2.1 эта конструкция вызывает предупреждения, а иногда и ошибки.
На уровне модуля¶At Module Level
Хотя использовать from module import * на уровне модуля допустимо, обычно это плохая идея. Во-первых, теряется важное свойство Python – можно узнать, где определено каждое имя верхнего уровня, с помощью простой функции «поиска» в вашем любимом редакторе. Вы также открываете себя для проблем в будущем, если какой-нибудь модуль обрастёт дополнительными функциями или классами.
Один из самых ужасных вопросов, задаваемых в новостной группе, – почему этот код:
f = open("www")
f.read()
не работает. Конечно, он отлично работает (при условии, что у вас есть файл с именем «www».) Но он не работает, если где-то в модуле присутствует оператор from
os import *. Модуль os содержит функцию open(), которая возвращает целое число. Хотя она очень полезна, затенение встроенной функции – одно из наименее полезных её свойств.
Помните, вы никогда не можете знать наверняка, какие имена экспортирует модуль, поэтому либо берите то, что вам нужно – from module import name1, name2, либо храните их в модуле и обращайтесь по мере необходимости – import module;print module.name.
Когда это вполне допустимо¶When It Is Just Fine
Бывают ситуации, когда from module import * вполне уместно:
Интерактивная подсказка. Например,
from math import *превращает Python в отличный научный калькулятор.При расширении модуля на C модулем на Python.
Когда модуль позиционирует себя как безопасный для
from import *.
Просто exec, execfile() и подобные¶Unadorned exec, execfile() and friends
Слово «просто» (unadorned) означает использование без явного словаря, и в этом случае эти конструкции выполняют код в текущем окружении. Это опасно по тем же причинам, что и from import * – оно может перезаписать переменные, на которые вы рассчитываете, и испортить остальной код. Просто не делайте так.
Плохие примеры:
>>> for name in sys.argv[1:]:
>>> exec "%s=1" % name
>>> def func(s, **kw):
>>> for var, val in kw.items():
>>> exec "s.%s=val" % var # недопустимо!
>>> execfile("handler.py")
>>> handle()
Хорошие примеры:
>>> d = {}
>>> for name in sys.argv[1:]:
>>> d[name] = 1
>>> def func(s, **kw):
>>> for var, val in kw.items():
>>> setattr(s, var, val)
>>> d={}
>>> execfile("handle.py", d, d)
>>> handle = d['handle']
>>> handle()
from module import name1, name2¶
Это «не делай», которое гораздо слабее предыдущих «не делай», но всё же то, чего не следует делать, если нет веских причин. Причина, по которой это обычно плохая идея, заключается в том, что у вас внезапно появляется объект, который живёт в двух разных пространствах имён. Когда привязка в одном пространстве имён меняется, привязка в другом не меняется, поэтому между ними возникает расхождение. Это происходит, например, когда один модуль перезагружается или изменяет определение функции во время выполнения.
Плохой пример:
# foo.py
a = 1
# bar.py
from foo import a
if something():
a = 2 # опасность: foo.a != a
Хороший пример:
# foo.py
a = 1
# bar.py
import foo
if something():
foo.a = 2
except:¶
В Python есть конструкция except:, которая перехватывает все исключения. Поскольку каждая ошибка в Python порождает исключение, использование except: может превратить многие ошибки программирования в проблемы времени выполнения, что затрудняет отладку.
Следующий код отлично демонстрирует, почему это плохо:
try:
foo = opne("file") # опечатка в "open"
except:
sys.exit("could not open file!")
Вторая строка вызывает NameError, которое перехватывается конструкцией except. Программа завершится, и сообщение об ошибке, которое она выведет, заставит вас думать, что проблема в читаемости "file", хотя на самом деле настоящая ошибка не имеет к "file" никакого отношения.
Более правильный способ написать вышеприведённый код –
try:
foo = opne("file")
except IOError:
sys.exit("could not open file")
При запуске Python выдаст трассировку, показывающую NameError, и сразу станет ясно, что нужно исправить.
Поскольку except: перехватывает все исключения, включая SystemExit, KeyboardInterrupt и GeneratorExit (которое не является ошибкой и обычно не должно перехватываться пользовательским кодом), использование голого except: почти никогда не является хорошей идеей. В ситуациях, когда нужно перехватить все «нормальные» ошибки, например, во фреймворке, запускающем колбэки, можно перехватить базовый класс для всех обычных исключений – Exception. К сожалению, в Python 2.x сторонний код может порождать исключения, не наследующие от Exception, поэтому в Python 2.x в некоторых случаях может потребоваться использовать голый except: и вручную повторно порождать исключения, которые не нужно перехватывать.
Исключения¶Exceptions
Исключения – полезная возможность Python. Стоит научиться возбуждать их всякий раз, когда происходит что-то неожиданное, и перехватывать только там, где можно на них повлиять.
Ниже приведён очень популярный антипаттерн.
def get_status(file):
if not os.path.exists(file):
print "file not found"
sys.exit(1)
return open(file).readline()
Рассмотрим случай, когда файл удаляется между вызовом os.path.exists() и вызовом open(). В этом случае последняя строка вызовет IOError. То же самое произойдёт, если файл существует, но нет прав на чтение. Поскольку тестирование на обычной машине на существующих и несуществующих файлах кажется безошибочным, результаты тестов будут выглядеть хорошо, и код будет выпущен. Позже необработанное IOError (или, возможно, другое EnvironmentError) дойдёт до пользователя, который увидит неприглядную трассировку.
Вот несколько более удачный способ.
def get_status(file):
try:
return open(file).readline()
except EnvironmentError as err:
print "Unable to open file: {}".format(err)
sys.exit(1)
В этой версии либо файл открывается и строка читается (поэтому он работает даже на ненадёжных NFS или SMB-соединениях), либо выводится сообщение об ошибке, содержащее всю доступную информацию о причине неудачи открытия, и приложение завершается.
Однако даже эта версия get_status() делает слишком много допущений – что она будет использоваться только в коротком скрипте, а не, скажем, в долго работающем сервере. Конечно, вызывающий код мог бы сделать что-то вроде
try:
status = get_status(log)
except SystemExit:
status = None
Но есть способ лучше. Следует стараться использовать как можно меньше конструкций except в своём коде – те, что используются, обычно находятся внутри вызовов, которые всегда должны завершаться успешно, или как всеобъемлющий перехват в главной функции.
Итак, ещё лучшая версия get_status(), вероятно,
def get_status(file):
return open(file).readline()
Вызывающий код может обработать исключение, если захочет (например, если он перебирает несколько файлов в цикле), или просто позволить исключению всплыть до своего вызывающего кода.
Но последняя версия всё ещё содержит серьёзную проблему – из-за особенностей реализации в CPython файл не будет закрыт при возникновении исключения до завершения обработчика исключения; и, что хуже, в других реализациях (например, Jython) он может вообще не закрыться, независимо от того, возникло исключение или нет.
Лучшая версия этой функции использует вызов open() в качестве менеджера контекста, что гарантирует закрытие файла сразу после возврата функции:
def get_status(file):
with open(file) as fp:
return fp.readline()
Использование встроенных средств¶Using the Batteries
Время от времени люди зачем-то снова пишут то, что уже есть в библиотеке Python, и обычно плохо. Хотя некоторые модули имеют неудачный интерфейс, в целом гораздо лучше использовать богатую стандартную библиотеку и типы данных, входящие в Python, чем изобретать свои собственные.
Полезный модуль, о котором знают очень немногие, – это os.path. Он всегда выполняет корректные операции с путями для вашей операционной системы и обычно будет намного лучше всего, что вы придумаете сами.
Сравните:
# фу!
return dir+"/"+file
# лучше
return os.path.join(dir, file)
Ещё полезные функции в os.path: basename(), dirname() и splitext().
Есть также много полезных встроенных функций, о которых люди почему-то не знают: min() и max() могут найти минимум/максимум любой последовательности с сопоставимой семантикой, но многие пишут свои собственные max()/min(). Ещё одна чрезвычайно полезная функция – reduce(), которую можно использовать для многократного применения бинарной операции к последовательности, сводя её к одному значению. Например, вычислить факториал с помощью серии операций умножения:
>>> n = 4
>>> import operator
>>> reduce(operator.mul, range(1, n+1))
24
Что касается разбора чисел, обратите внимание, что float(), int() и long() все принимают строковые аргументы и будут отклонять некорректные строки, порождая ValueError.
Использование обратного слеша для продолжения инструкций¶Using Backslash to Continue Statements
Поскольку Python рассматривает перевод строки как завершитель инструкции, и поскольку инструкции часто длиннее, чем удобно помещать в одну строку, многие делают так:
if foo.bar()['first'][0] == baz.quux(1, 2)[5:9] and \
calculate_number(10, 20) != forbulate(500, 360):
pass
Следует понимать, что это опасно: случайный пробел после \ сделает эту строку неверной, а случайные пробелы, как известно, трудно заметить в редакторах. В этом случае будет хотя бы синтаксическая ошибка, но если бы код был:
value = foo.bar()['first'][0]*baz.quux(1, 2)[5:9] \
+ calculate_number(10, 20)*forbulate(500, 360)
то ошибка была бы неявной.
Обычно гораздо лучше использовать неявное продолжение внутри скобок:
Эта версия защищена от ошибок:
value = (foo.bar()['first'][0]*baz.quux(1, 2)[5:9]
+ calculate_number(10, 20)*forbulate(500, 360))