Документация Python неофициальный перевод
Содержание страницы

Идиомы и анти-идиомы в PythonIdioms 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 * is invalid inside function definitions. While many versions of Python do not check for the invalidity, it does not make it more valid, no more then having a smart lawyer makes a man innocent. Do not use it like that ever. Even in versions where it was accepted, it made the function execution slower, because the compiler could not be certain which names are local and which are global. In Python 2.1 this construct causes warnings, and sometimes even errors.

На уровне модуля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 вызывает исключение, это приводит к тому, что многие ошибки программирования выглядят как проблемы времени выполнения, и затрудняет процесс отладки.

Следующий код показывает наглядный пример:

try:
    foo = opne("file") # опечатка в "open"
except:
    sys.exit("could not open file!")

Вторая строка вызывает исключение NameError, которое перехватывается блоком except. Программа завершится, и вы не будете знать, что это не имеет никакого отношения к читаемости "file".

Лучше написать так:

try:
    foo = opne("file") # будет изменён на "open", как только будет запущен
except IOError:
    sys.exit("could not open file")

Есть несколько ситуаций, в которых конструкция 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. То же самое произойдёт, если file существует, но нет прав на чтение. Поскольку при тестировании на обычной машине с существующими и несуществующими файлами ошибка не проявляется, результаты тестирования будут выглядеть нормально, и код будет выпущен. Тогда необработанное исключение IOError доходит до пользователя, который вынужден смотреть на некрасивую трассировку.

Вот более правильный способ:

def get_status(file):
    try:
        return open(file).readline()
    except (IOError, OSError):
        print("file not found")
        sys.exit(1)

В этой версии либо файл открывается и строка читается (так что это работает даже на ненадёжных соединениях NFS или SMB), либо выводится сообщение и приложение завершается.

Тем не менее, get_status() делает слишком много предположений – что она будет использоваться только в коротком скрипте, а не, скажем, в долго работающем сервере. Конечно, вызывающий код мог бы сделать что-то вроде

try:
    status = get_status(log)
except SystemExit:
    status = None

Поэтому старайтесь использовать как можно меньше конструкций except в коде – обычно они нужны как универсальный обработчик в main() или внутри вызовов, которые всегда должны завершаться успешно.

Итак, вероятно, лучший вариант:

def get_status(file):
    return open(file).readline()

Вызывающий код может обработать исключение, если захочет (например, если он перебирает несколько файлов в цикле), или просто позволить исключению всплыть к своему вызывающему.

Последний вариант тоже не очень хорош – из-за особенностей реализации файл не будет закрыт при возникновении исключения, пока не завершится обработчик, а возможно, и вовсе не будет закрыт в реализациях, отличных от C (например, Jython).

def get_status(file):
    fp = open(file)
    try:
        return fp.readline()
    finally:
        fp.close()

Использование встроенных средств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(). Классическое использование reduce() выглядит примерно так:

import sys, operator, functools
nums = list(map(float, sys.argv[1:]))
print(functools.reduce(operator.add, nums) / len(nums))

Этот маленький скрипт выводит среднее значение всех чисел, переданных в командной строке. reduce() суммирует все числа, а остальное – просто подготовка и обработка результатов.

Кстати, обратите внимание, что float() и int() принимают аргументы строкового типа и поэтому подходят для разбора – при условии, что вы готовы обрабатывать 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))