Содержание страницы
Идиомы и анти-идиомы в 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 *.
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.
Исключения¶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. То же самое произойдёт если file существует, но нет прав на чтение. Поскольку тестирование на обычной машине на существующих и несуществующих файлах не выявляет ошибок, результаты тестов будут казаться нормальными, и код попадёт в релиз. Позже необработанное IOError (или, возможно, другое EnvironmentError) доходит до пользователя, который видит уродливый traceback.
Вот несколько более удачный способ.
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(). Ещё одна очень полезная функция – functools.reduce(), которая применяет бинарную операцию к последовательности, сворачивая её до одного значения. Например, вычисление факториала через последовательное умножение:
>>> n = 4
>>> import operator, functools
>>> functools.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))