Книга: Автостопом по Python
Назад: Часть II. Переходим к делу
Дальше: 5. Читаем отличный код

4. Пишем отличный код

В этой главе продемонстрированы лучшие приемы написания отличного кода Python. Мы рассмотрим соглашения, связанные со стилем написания кода, а также правила хорошего тона, связанные с журналированием, перечислим основные отличия между доступными лицензиями для открытого исходного кода. Это поможет вам писать код, который впоследствии можно будет легко использовать и расширять.

Стиль кода

Питонисты (ветераны разработки на Python) рады тому, что их язык настолько понятен, — люди, которые никогда не занимались разработкой, способны разобраться в работе программы при чтении ее исходного кода. Легкость чтения лежит в основе дизайна Python (важно понимать, что написанный код будет прочитан много раз).

Одна из причин, почему код Python прост для понимания, заключается в информативном руководстве по стилю написания кода (оно представлено в двух Предложениях по развитию Python (Python Enhancement Proposal) PEP 20 и PEP 8; о них скажем пару слов) и питонских идиомах. Если питонист указывает на фрагмент кода и говорит, что он не питонский, это обычно означает, что строки не соответствуют распространенным принципам и не являются читаемыми. Конечно, «глупая последовательность — пугало маленьких умов». Педантичное следование PEP может снизить читаемость и понятность.

PEP 8

PEP 8 де-факто представляет собой руководство по стилю написания кода Python. В нем рассматриваются соглашения по именованию, структура кода, пустые области (табуляция против пробелов) и другие аналогичные темы.

Мы рекомендуем изучить его. Все сообщество Python старается следовать принципам, изложенным в этом документе. Некоторые проекты время от времени могут отступать от него, а другие (вроде Requests — ) — добавлять поправки к рекомендациям.

Писать код с учетом принципов PEP 8 — хорошая идея (помогает разработчикам создавать более стабильный код). С помощью программы pep8 (), которая запускается из командной строки, можно проверить код на соответствие принципам PEP 8. Для установки этой программы введите в терминале такую команду:

$ pip3 install pep8

Рассмотрим пример того, что вы можете увидеть при запуске команды pep8:

$ pep8 optparse.py

optparse.py:69:11: E401 multiple imports on one line

optparse.py:77:1: E302 expected 2 blank lines, found 1

optparse.py:88:5: E301 expected 1 blank line, found 0

optparse.py:222:34: W602 deprecated form of raising exception

optparse.py:347:31: E211 whitespace before '('

optparse.py:357:17: E201 whitespace after '{'

optparse.py:472:29: E221 multiple spaces before operator

optparse.py:544:21: W601 .has_key() is deprecated, use 'in'

Большинство недостатков можно легко исправить, рекомендации по их устранению даются в PEP 8. В руководстве по стилю написания кода для Requests приведены примеры хорошего и плохого кода (лишь немного отличаются от оригинального PEP 8).

Инструменты контроля качества кода, о которых мы говорили в разделе «Текстовые редакторы» в главе 3, обычно используют программу pep8, поэтому вы также можете установить один из них для проверки кода внутри редактора или IDE. Или же можете выбрать команду auto pep8, которая автоматически переформатирует код согласно PEP 8. Установить ее можно так:

$ pip3 install autopep8

Чтобы переформатировать файл (перезаписав оригинал), введите следующую команду:

$ autopep8 --in-place optparse.py

Если вы не добавите флаг --in-place, это заставит программу вывести модифицированный код в консоль (или записать в другой файл). Флаг --aggressive выполнит более существенные изменения, его можно применить несколько раз для получения значительного эффекта.

PEP 20 (также известный как «Дзен Питона»)

PEP 20 (/) (набор принципов для принятия решений в Python) всегда доступен по команде import this в оболочке Python. Несмот­ря на название, PEP 20 содержит 19 афоризмов, а не 20 (последний не был записан).

Реальная история «Дзена Питона» увековечена в статье Барри Уорсоу (Barry Warsaw) Import this and the Zen of Python ().

Дзен Питона. Автор Тим Питерс

Красивое лучше, чем уродливое.

Явное лучше, чем неявное.

Простое лучше, чем сложное.

Сложное лучше, чем запутанное.

Одноуровневое лучше, чем вложенное.

Разреженное лучше, чем плотное.

Читаемость имеет значение.

Особые случаи не настолько особые, чтобы нарушать правила.

При этом практичность важнее безупречности.

Ошибки никогда не должны замалчиваться.

Если не замалчиваются явно.

Встретив двусмысленность, отбрось искушение угадать.

Должен существовать один — и желательно только один — очевидный способ сделать это.

Хотя он поначалу может быть и не очевиден, если вы не голландец.

Сейчас лучше, чем никогда.

Хотя никогда зачастую лучше, чем прямо сейчас.

Если реализацию сложно объяснить — идея плоха.

Если реализацию легко объяснить — идея, возможно, хороша.

Пространства имен — отличная штука! Будем делать их побольше!

Для того чтобы увидеть пример использования каждого из этих афоризмов, обратитесь к презентации Хантера Блэнкса (Hunter Blanks) PEP 20 (The Zen of Python) by Example (). Рэймонд Хеттингер (Raymond Hettinger) также демонстрирует применение этих принципов в своей речи Beyond PEP 8: Best Practices for Beautiful, Intelligible Code ().

Общие советы

В этом разделе приводятся концепции, связанные со стилем (надеемся, вы с ними согласитесь). Зачастую они применимы и к другим языкам. Некоторые следуют непосредственно из «Дзена Питона», другие основаны на здравом смысле. Они подтверждают наш принцип работы: при написании кода Python выбирать наиболее очевидный способ его представления из имеющихся вариантов.

Явное лучше, чем неявное

В Python предпочтителен наиболее явный способ выражения:

Плохой код

Хороший код

def make_dict(*args):

    x, y = args

    return dict(**locals())

def make_dict(x, y):

    return {'x': x, 'y': y}

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

Разреженное лучше, чем плотное

В каждой строке размещайте только одно выражение. Использование сложных выражений (вроде абстракция списков (иначе называют списковыми включениями — list comprehensions)) позволяется и даже поощряется за их краткость и выразительность, но признаком хорошего тона будет размещение отдельных выражений на разных строках. Это поможет создавать более простые для понимания разности, когда подобное выражение изменяется:

Плохой код

Хороший код

print('one'); print('two')

print('one')

print('two')

if x == 1: print('one')

if x == 1:

    print('one')

if (<complex comparison> and

    <other complex comparison>):

    # сделать что-нибудь

cond1 = <complex comparison>

cond2 = <other complex comparison>

if cond1 and cond2:

    # сделать что-нибудь

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

Ошибки никогда не должны замалчиваться/Если не замалчиваются явно

Обработка ошибок в Python выполняется с помощью выражения try. Пример из пакета HowDoI (более подробно описывается в разделе «HowDoI» в главе 5) Бена Глейтсмана (Ben Gleitzman) показывает, когда замалчивать ошибки приемлемо:

def format_output(code, args):

    if not args['color']:

        return code

    lexer = None

    # попробуем отыскать лексеры с помощью тегов Stack Overflow

    # или аргументов query

    for keyword in args['query'].split() + args['tags']:

        try:

            lexer = get_lexer_by_name(keyword)

            break

        except ClassNotFound:

            pass

    # лексер не найден, пробуем угадать

    if not lexer:

        lexer = guess_lexer(code)

    return highlight(code,

                     lexer,

                     TerminalFormatter(bg='dark'))

Перед вами часть пакета, который предоставляет сценарий командной строки, позволяющий найти в Интернете (по умолчанию на сайте Stack Overflow) способ выполнить задачу по программированию. Функция format_output() подсвечивает синтаксис, просматривая теги вопроса на предмет строки, которую смог разобрать лексер (также он называется токенайзером; теги python, java или bash позволят определить лексер, который нужно использовать для разбиения и подсвечивания кода), а затем, если он даст сбой, пробует определить язык по самому коду. Когда программа достигает оператора try, она может пойти по одному из трех путей:

поток выполнения входит в блок try (весь код, расположенный между try и except), лексер успешно определяется, цикл прерывается, и функция возвращает код, подсвеченный с помощью выбранного лексера;

• лексер не найден, генерируется и обрабатывается исключение ClassNotFound — и ничего не происходит. Цикл продолжит выполнение до тех пор, пока не завершится самостоятельно или не будет найден лексер;

генерируется какое-то другое исключение (например, KeyboardInterrupt), которое не обрабатывается и поднимается на верхний уровень, останавливая выполнение.

Часть афоризма «не замалчиваются» препятствует чрезмерному выявлению ошибок. Рассмотрим пример (можете попробовать запустить его в отдельном окне консоли — так будет проще прервать выполнение, когда вы во все вникнете):

>>> while True:

...     try:

...         print("nyah", end=" ")

...     except:

...         pass

Или не пробуйте запускать его. Поскольку для блока except не указано конкретное исключение, он будет отлавливать все исключения, в том числе KeyboardInterrupt (Ctrl+C в консоли POSIX), и игнорировать их. Соответственно, он проигнорирует множество ваших попыток прервать его работу. Это не просто проблема с прерывания­ми — блок except также может скрывать ошибки, что вызовет проблемы в будущем (их станет трудно диагностировать). Поэтому не замалчивайте ошибки: всегда явно указывайте имена исключений, которые хотите поймать, и обрабатывайте только их. Если вы хотите просто записать в журнал или как-то еще убедиться в наличии исключения и вызвать его повторно, как в следующем сниппете, тогда все в порядке. Только не замалчивайте ошибки (не обрабатывая их и не вызывая повторно):

>>> while True:

...     try:

...         print("ni", end="-")

...     except:

...         print("An exception happened. Raising.")

...         raise

Аргументы функций должны быть интуитивно понятными

Ваш выбор при дизайне API определит последующую возможность взаимодействовать с функцией. Аргументы можно передавать в функции четырьмя разными способами.

10477.png 

10482.png Позиционные аргументы обязательны и не имеют значений по умолчанию.

10493.png Аргументы с ключевым словом необязательны и имеют значения по умолчанию.

10505.png Список с произвольным количеством аргументов необязателен и не имеет значений по умолчанию.

10516.png Словарь с произвольным количеством аргументов с ключевым словом необязателен и не имеет значений по умолчанию.

Рассмотрим, когда можно использовать каждый метод передачи аргументов.

Позиционные аргументы. Применяйте этот метод, когда у вас всего несколько аргументов для функции, которые являются частью ее значения и имеют правильный порядок. Например, пользователь без труда вспомнит, что у функций send(message, recipient) или point(x, y) должны быть два аргумента, а также порядок этих аргументов.

Антишаблон: при вызове функций можно поменять местами имена аргументов, например так: send(recipient="World", message="The answer is 42.") и point(y=2, x=1). Это снижает читаемость. Используйте более понятные вызовы send("The answer is 42", "World") и point(1, 2).

• Аргументы с ключевым словом. Когда функция имеет более двух или трех позиционных параметров, ее сигнатуру сложнее запомнить. В этом случае можно применить аргументы с ключевым словом, которые имеют значения по умолчанию. Например, более полная версия функции send может иметь сигнатуру send(message, to, cc=None, bcc=None). Здесь параметры cc и bcc являются необязательными и равны None, если для них не получено значение.

Антишаблон: можно отправить аргументы в правильном порядке, но не указывать их имена явно, например send("42", "Frankie", "Benjy", "Trillian"), переслав скрытую копию пользователю с именем Триллиан. Можно также передать именованные аргументы в неправильном порядке, например send("42", "Frankie", bcc="Trillian", cc="Benjy"). Если у вас нет веской причины делать это, лучше всего использовать вариант, приближенный к определению функции: send("42", "Frankie", cc="Benjy", bcc="Trillian").

10529.png

Никогда лучше, чем сейчас

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

• Список с произвольным количеством аргументов. Такой список определяется с помощью конструкции *args, которая указывает на произвольное количество позиционных аргументов. В теле функции args будет играть роль кортежа, состоящего из всех оставшихся позиционных аргументов. Например, функция send(message, *args) также может быть вызвана, когда каждый получатель будет представлен отдельным аргументом: send("42", "Frankie", "Benjy", "Trillian"). В теле функции конструкция args будет равна выражению ("Frankie", "Benjy", "Trillian"). Хороший пример, иллюстрирующий этот подход, — функция print.

Подводный камень: если функция получает список аргументов одного вида, более понятным будет использование списка или любой другой последовательности. Если функция send в этом примере принимает несколько получателей, мы определим ее явно как send(message, recipients) и будем вызывать как send("42", ["Benjy", "Frankie", "Trillian"]).

• Словарь с произвольным количеством аргументов с ключевым словом. Такой словарь определяется с помощью конструкции **kwargs, которая указывает на произвольное количество именованных аргументов. В теле функции kwargs будет словарем, содержащим все переданные именованные аргументы, которые не были «пойманы» другими аргументами с ключевым словом в сигнатуре функции. Это может быть полезно при журналировании. Средства форматирования на разных уровнях могут принять необходимую им информацию, минуя пользователя.

Подводный камень: эти мощные приемы нужно применять только в том случае, когда это действительно необходимо. Если же имеется более простая и прозрачная конструкция, то для выражения предназначения функции следует выбрать именно ее.

10543.png

Имена переменных *args и **kwargs могут (и должны быть) заменены другими, если это более информативно.

Какие аргументы станут позиционными, а какие — необязательными, зависит только от программиста, который пишет функцию. От него также зависит наличие передачи произвольного количества аргументов. В конце концов, должен существовать один (предпочтительно всего один) очевидный способ это сделать. Другие пользователи оценят ваши усилия, если функции, написанные на Python:

легко прочитать (имя и аргументы не требуют объяснения);

легко изменить (добавление нового аргумента с ключевым словом не разрушит другие части кода).

Если реализацию сложно объяснить — идея плоха

Python поставляется с богатым набором инструментов (за что его любят хакеры), который позволяет вам делать абсолютно невероятные вещи, например:

изменять способ создания объектов;

• изменять способ импортирования модулей Python;

встраивать в Python подпрограммы, написанные на С.

Все эти действия имеют недостатки, поэтому всегда лучше выбирать прямолинейный способ достижения цели. Основной минус: при использовании подобных конструкций снижается читаемость, поэтому то, что вы получаете в результате, должно быть более важным, чем потеря читаемости. Многие инструменты, предназначенные для анализа кода, не смогут работать с таким «волшебным» кодом.

Разработчик Python должен знать о таких практически бесконечных возможностях, поскольку это вселяет уверенность в том, что нерешаемых проблем не существует. Однако важно знать, как и когда применять эти знания нельзя.

Как и мастера кун-фу, питонисты знают, как можно убить одним пальцем, и никогда этого не делают.

Мы все — ответственные пользователи

Как уже демонстрировалось, с помощью Python можно делать многое, но некоторые приемы потенциально могут быть опасными. В частности, любой клиентский код может переопределить свойства и методы объекта: в Python нет ключевого слова private. Эта философия сильно отличается от той, что присуща высокозащищенным языкам вроде Java, — они имеют множество механизмов, предотвращающих неверное использование. Философия Python сосредоточена во фразе «Мы все — ответственные пользователи».

Это не значит, что ни одно свойство не считается закрытым и что в Python нельзя реализовать инкапсуляцию. Наоборот, вместо того чтобы возводить бетонные стены между своим и чужим кодом, сообщество Python предпочитает полагаться на набор соглашений, которые указывают, к каким элементам нельзя получить доступ напрямую.

Основным соглашением для закрытых свойств и деталей реализации является добавление к именам всех подобных элементов нижнего подчеркивания (например, sys._getframe). Если клиентский код нарушает это правило и получает доступ к отмеченным элементам, будет считаться, что любое неверное поведение или проб­лемы вызваны именно клиентским кодом.

Использование этой концепции всеми одобряется: имя любого метода или свойства, к которым клиентский код не должен получить доступ, должно начинаться с нижнего подчеркивания. Это гарантирует более качественное разделение обязанностей и упрощает внесение изменений в код. Всегда можно сделать закрытое свойство открытым, обратное же действие выполнить гораздо сложнее.

Возвращайте значения из одной точки

Когда сложность функции увеличивается, зачастую вы можете встретить несколько выражений return в теле этой функции. Однако для того, чтобы ее было проще понять и прочесть, возвращайте осмысленные значения из минимально возможного количества точек.

Выйти из функции можно в двух случаях: при появлении ошибки или при возвращении значения после того, как функция нормально отработает. Когда функция не может работать корректно, уместно вернуть значение None или False. В этом случае лучше вернуть значение из функции максимально рано после его обнаружения, дабы упростить структуру функции: весь код, который находится после выражения возврата-в-случае-сбоя, будет считать, что все условия соблюдены, и продолжит вычисление основного результата функции. Необходимы несколько подобных выражений return.

Однако везде, где это возможно, имейте только одну точку выхода — сложно выполнять отладку для функций, когда вам сначала нужно определить, какое выражение return ответственно за результат. Наличие единой точки выхода из функции также поможет избавиться от некоторых ветвей кода, поскольку наличие пары точек выхода, возможно, намекает на то, что необходимо провести подобный рефакторинг. Код в следующем примере нельзя назвать плохим, но его можно сделать более чистым (как это показано в комментариях):

def select_ad(third_party_ads, user_preferences):

    if not third_party_ads:

        return None  # Лучше сгенерировать исключение

    if not user_preferences:

        return None  # Лучше сгенерировать исключение

    # Сложный код, предназначенный для выбора best_ad

    # Из доступных вариантов на основе индивидуальных предпочтений...

    # Постарайтесь устоять перед искушением вернуть best_ad в случае успеха...

    if not best_ad:

        # Запасной план определения best_ad

    return best_ad  # Единая точка выхода, которая поможет обслуживать код

Соглашения

Соглашения важны для всех, но это не единственный способ решения задачи. Соглашения, приведенные в этом разделе, довольно распространены, и мы рекомендуем придерживаться их, чтобы сделать свой код более читаемым.

Альтернативы при проверке на равенство

Если вам не нужно явно сравнивать свое значение со значением True, None или 0, вы можете добавить его к оператору if, как в следующих примерах (см. статью «Проверка значения на правдивость» () — там представлен список значений, которые расцениваются как False).

Плохой код

Хороший код

if attr == True:

    print 'True!'

# Просто проверяем значение

if attr:

    print 'attr is truthy!'

# или проверяем на противоположное значение

if not attr:

    print 'attr is falsey!'

# если вам нужно только значение 'True'

if attr is True:

    print 'attr is True'

if attr == None:

    print 'attr is None!'

# или явно проверяем на значение None

if attr is None:

    print 'attr is None!'

Получаем доступ к элементам массива

Используйте синтаксис x in d вместо метода dict.has_key или передавайте аргумент по умолчанию в метод dict.get().

Плохой код

Хороший код

>>> d = {'hello': 'world'}

>>>

>>> if d.has_key('hello'):

...     print(d['hello'])  

# prints 'world'

... else:

...     print('default_value')

...

world

>>> d = {'hello': 'world'}

>>>

>>> print d.get('hello', 'default_value')

world

>>> print d.get('howdy', 'default_value')

default_value

>>>

>>> # или:

... if 'hello' in d:

...     print(d['hello'])

...

world

Манипуляции со списками

Списковые включения — мощный способ работы со списками (для получения более подробной информации обратитесь к соответствующей статье в руководстве The Python Tutorial по адресу ). Функции map() и filter() могут выполнять операции со списками с помощью другого, более выразительного синтаксиса.

Стандартный цикл

Списковое включение

# Отфильтруем все элементы,

# чье значение превышает 4

a = [3, 4, 5]

b = []

for i in a:

    if i > 4:

        b.append(i)

# Списковое включение выглядит

# прозрачнее

a = [3, 4, 5]

b = [i for i in a if i > 4]

# Или:

b = filter(lambda x: x > 4, a)

# Добавим 3 к каждому элементу списка

a = [3, 4, 5]

for i in range(len(a)):

    a[i] += 3

# Здесь также прозрачнее

a = [3, 4, 5]

a = [i + 3 for i in a]

# Или:

a = map(lambda i: i + 3, a)

Используйте функцию enumerate(), чтобы определить свою позицию в списке. Этот вариант выглядит более читаемым, чем создание счетчика, и лучше оптимизирован для итераторов:

>>> a = ["icky", "icky", "icky", "p-tang"]

>>> for i, item in enumerate(a):

...     print("{i}: {item}".format(i=i, item=item))

...

0: icky

1: icky

2: icky

3: p-tang

Продолжение длинной строки кода

Когда логическая строка кода длиннее принятого значения, нужно разбить строку на несколько физических строк. Интерпретатор Python объединит следующие друг за другом строки, если последний символ строки — обратный слэш. В некоторых случаях это может оказаться полезным, но такого подхода следует избегать, потому что знак пробела, добавленный в конце строки, разрушит код и может привести к неожиданным последствиям.

Лучшее решение — заключить элементы в круглые скобки. Если интерпретатор Python встретит незакрытую круглую скобку в одной строке, он будет присоединять к ней следующие строки до тех пор, пока скобка не будет закрыта. То же поведение верно для фигурных и квадратных скобок.

Плохой код

Хороший код

french_insult = \

"Your mother was a hamster, and \

your father smelt of elderberries!"

french_insult = (

    "Your mother was a hamster, and "

    "your father smelt of elderberries!"

)

from some.deep.module.in.a.module \

    import a_nice_function, \

        another_nice_function, \

        yet_another_nice_function

from some.deep.module.in.a.module import (

    a_nice_function,

    another_nice_function,

    yet_another_nice_function

)

Однако зачастую необходимость разбивать длинные логические строки указывает на то, что вы пытаетесь выполнить слишком много действий за раз, что может навредить читаемости.

Идиомы

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

Распаковка

Если вы знаете длину списка или кортежа, можете присвоить имена их элементам с помощью распаковки. Поскольку вы можете указать количество разбиений строки для функций split() и rsplit(), правую сторону выражения присваивания можно разбить только один раз (например, на имя файла и расширение), а левая сторона может содержать оба места назначения одновременно, в правильном порядке. Например, так:

>>> filename, ext = "my_photo.orig.png".rsplit(".", 1)

>>> print(filename, "is a", ext, "file.")

my_photo.orig is a png file.

Вы можете задействовать распаковку для того, чтобы менять местами переменные:

a, b = b, a

Вложенная распаковка также работает:

a, (b, c) = 1, (2, 3)

В Python 3 в PEP 3132 (/) был представлен новый метод расширенной распаковки:

a, *rest = [1, 2, 3]

# a = 1, rest = [2, 3]

 

a, *middle, c = [1, 2, 3, 4]

# a = 1, middle = [2, 3], c = 4

Игнорирование значения

Если вам необходимо присвоить какое-то значение во время распаковки, но сама переменная не нужна, воспользуйтесь двойным подчеркиванием (__):

filename = 'foobar.txt'

basename, __, ext = filename.rpartition('.')

10602.png

Многие руководства по стилю для Python рекомендуют использовать одинарное подчеркивание (_) для подобных переменных вместо двойного (__), о котором говорится здесь. Проблема в том, что одинарное подчеркивание зачастую применяется как псевдоним для функции gettext.gettext() и как интерактивное приглашение сохранить значение последней операции. Двойное подчеркивание выглядит точно так же прозрачно и почти так же удобно, снижает риск случайного переписывания переменной с именем «_» в обоих сценариях.

Создание списка длиной N, состоящего из одинаковых значений

Используйте оператор списка Python * для того, чтобы создать список, состоящий из одинаковых неизменяемых элементов:

>>> four_nones = [None] * 4

>>> print(four_nones)

[None, None, None, None]

Одинаковые объекты должны иметь одинаковые значения хэша. В документации к Python содержится более подробная информация.

Однако будьте осторожны при работе с изменяемыми объектами: поскольку списки изменяемы, оператор * создаст список, состоящий из N ссылок на него самого, и это вряд ли вас устроит. Поэтому используйте списковое включение:

Плохой код

Хороший код

>>> four_lists = [[]] * 4

>>> four_lists[0].append("Ni")

>>> print(four_lists)

[['Ni'], ['Ni'], ['Ni'], ['Ni']]

>>> four_lists = [[] for __ in range(4)]

>>> four_lists[0].append("Ni")

>>> print(four_lists)

[['Ni'], [], [], []]

Распространенная идиома для создания строк состоит в том, чтобы использовать функцию str.join() для пустой строки. Данная идиома может быть применена к спискам и кортежам:

>>> letters = ['s', 'p', 'a', 'm']

>>> word = ''.join(letters)

>>> print(word)

spam

Иногда требуется выполнить поиск по коллекции элементов. Изучим два варианта: списки и множества.

Для примера рассмотрим следующий код:

>>> x = list(('foo', 'foo', 'bar', 'baz'))

>>> y = set(('foo', 'foo', 'bar', 'baz'))

>>>

>>> print(x)

['foo', 'foo', 'bar', 'baz']

>>> print(y)

{'foo', 'bar', 'baz'}

>>>

>>> 'foo' in x True

>>> 'foo' in y True

Даже несмотря на то что обе булевых проверки на наличие в списке и множестве выглядят идентично, а foo in y учитывает тот факт, что множества (и словари) в Python являются хэш-таблицами, производительность для этих двух примеров будет различной. Python должен пройти по каждому элементу списка в поисках совпадения, на что уходит много времени (это заметно при увеличении размера коллекций). Но поиск ключей во множестве может быть выполнен быстро с помощью поиска по хэшу. Кроме того, множества и словари не могут содержать повторяющихся записей и идентичных ключей. Для получения более подробной информации поинтересуйтесь семинаром на эту тему на ресурсе Stack Overflow ( 13882).

Контексты с гарантией безопасности по исключениям

Зачастую блоки try/finally используются для управления ресурсами вроде файлов или блокировок потоков в случае генерации исключений. В PEP 343 (/) представлены оператор with и протокол управления контекстом (в версиях 2.5 и выше) — идиома, позволяющая заменить блоки try/finally на более читаемый код. Протокол состоит из двух методов, __enter__() и __exit__(), которые при реализации для объекта позволяют использовать этот объект в операторе with, например так:

>>> import threading

>>> some_lock = threading.Lock()

>>>

>>> with some_lock:

...     # Создать Землю 1, запустить ее на десять миллионов лет ...

...     print(

...         "Look at me: I design coastlines.\n"

...         "I got an award for Norway."

...     )

...

Раньше это выглядело бы так:

>>> import threading

>>> some_lock = threading.Lock()

>>>

>>> some_lock.acquire()

>>> try:

...     # Создать Землю 1, запустить ее на десять миллионов лет ...

...     print(

...         "Look at me: I design coastlines.\n"

...         "I got an award for Norway."

...     )

... finally:

...     some_lock.release()

Модуль стандартной библиотеки contextlib () предоставляет дополнительные инструменты, которые помогают преобразовать функции в менеджеры контекстов, навязать вызов метода close(), подавить исключения (в Python 3.4 и выше) и перенаправить стандартные потоки вывода и ошибок (в Python 3.4, 3.5 и выше). Рассмотрим пример использования функции contextlib.closing():

>>> from contextlib import closing

>>> with closing(open("outfile.txt", "w")) as output:

...     output.write("Well, he's...he's, ah...probably pining for the fjords.")

...

56

Но поскольку методы __enter__() и __exit__() определены для объекта, который отвечает за ввод/вывод для файла, мы можем использовать это выражение непосредственно, не закрывая файл самостоятельно:

>>> with open("outfile.txt", "w") as output:

    output.write(

       "PININ' for the FJORDS?!?!?!? "

       "What kind of talk is that?, look, why did he fall "

       "flat on his back the moment I got 'im home?\n"

    )

...

123

Распространенные подводные камни

По большей части Python — чистый и надежный язык. Однако некоторые ситуации могут быть непонятны для новичков: какие-то из них созданы намеренно, но все равно могут удивить, другие можно считать особенностями языка. В целом все, что продемонстрировано в этом подразделе, относится к неоднозначному поведению, которое может показаться странным на первый взгляд, но впоследствии выглядит разумным (когда вы узнаете о причинах).

Изменяемые аргументы по умолчанию

Наиболее частый сюрприз, с которым сталкиваются новые программисты Python, — это отношение Python к изменяемым аргументам по умолчанию в определениях функции.

Что вы написали:

def append_to(element, to=[]):

    to.append(element)

    return to

Чего вы ожидаете:

my_list = append_to(12)

print(my_list)

my_other_list = append_to(42)

print(my_other_list)

Новый список создается всякий раз, когда вызывается функция, если второй аргумент не предоставлен, поэтому результат работы функции выглядит так:

[12]

[42]

Что происходит на самом деле:

[12]

[12, 42]

Новый список создается при определении функции, он же используется в момент каждого последующего вызова: аргументы по умолчанию в Python оцениваются при определении функции, а не при каждом ее вызове (как это происходит, например, в Ruby).

Это означает, что если вы используете изменяемый по умолчанию аргумент и измените его, то он изменится для всех последующих вызовов этой функции.

Что вам нужно сделать вместо этого? Создавайте новый объект при каждом вызове функции, используя аргумент по умолчанию, чтобы показать, что аргумент не был передан (в качестве такого значения подойдет None):

def append_to(element, to=None):

    if to is None:

        to = []

    to.append(element)

    return to

Когда подводный камень вовсе не подводный камень. Иногда вы можете намеренно задействовать (то есть использовать в качестве нормального варианта поведения) этот подводный камень, чтобы сохранять состояние между вызовами функции. Зачастую это делается при написании функции кэширования (которая сохраняет результаты в памяти), например:

def time_consuming_function(x, y, cache={}):

    args = (x, y)

    if args in cache:

        return cache[args]

    # В противном случае функция работает с аргументами в первый раз.

    # Выполняем сложную операцию...

    cache[args] = result

    return result

Замыкания с поздним связыванием

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

Что вы написали:

def create_multipliers():

    return [lambda x : i * x for i in range(5)]

Чего вы ожидаете:

for multiplier in create_multipliers():

    print(multiplier(2), end=" ... ")

print()

Список, содержащий пять функций, каждая из них имеет собственную замкнутую переменную i, которая умножается на их аргумент, что приводит к получению следующего результата:

0 ... 2 ... 4 ... 6 ... 8 ...

Что происходит на самом деле:

8 ... 8 ... 8 ... 8 ... 8 ...

Создаются пять функций, все они умножают х на 4. Почему? В Python замыкания имеют позднее связывание. Это говорит о том, что значения переменных, использованных в замыканиях, определяются в момент вызова внутренней функции.

В нашем примере, когда вызывается любая из возвращенных функций, значение переменной i определяется с помощью окружающей области видимости в момент вызова. К этому моменту цикл завершает свою работу и i получает итоговое значение 4.

Особенно неудобно то, что вам может показаться, будто ошибка как-то связана с лямбда-выражениями (). Функции, создаваемые с помощью лямбда-выражений, не отличаются от других. Фактически то же самое поведение проявляется и при использовании самого обычного def:

def create_multipliers():

    multipliers = []

    for i in range(5):

        def multiplier(x):

            return i * x

        multipliers.append(multiplier)

    return multipliers

Что вам нужно сделать вместо этого? Наиболее общее решение, возможно, станет «костылем» — временным вариантом устранения проблемы. Из-за уже упомянутого поведения Python, связанного с определением аргументов по умолчанию для функций (см. предыдущий пункт «Изменяемые аргументы функций»), вы можете создать замыкание, которое немедленно связывается со своими аргументами с помощью аргумента по умолчанию:

def create_multipliers():

    return [lambda x, i=i : i * x for i in range(5)]

Помимо этого вы можете использовать функцию functools.partial():

from functools import partial

from operator import mul

def create_multipliers():

    return [partial(mul, i) for i in range(5)]

Когда подводный камень вовсе не подводный камень. Иногда нужно, чтобы замыкания вели себя подобным образом. Позднее связывание может быть полезным во многих ситуациях (например, в проекте Diamond, см. пункт «Пример использования замыкания (когда подводный камень вовсе не подводный камень)» на с. 136). Наличие уникальных функций в циклах, к сожалению, может привести к сбоям.

Структурируем проект

Под структурированием мы понимаем решения, которые вы принимаете по поводу функционирования вашего проекта. Его цель состоит в использовании возможностей Python для создания чистого и эффективного кода. На практике это означает, что логика и зависимости в коде и структуре файлов и каталогов прозрачны.

По какому принципу функции должны размещаться в модулях? Как данные перемещаются по проекту? Какие функции могут быть сгруппированы и изолированы? Отвечая на эти вопросы, вы можете запланировать, как будет выглядеть ваш конечный продукт.

В книге Python Cookbook есть глава, посвященная модулям и пакетам (), в которой подробно описывается, как работают выражения __import__ и упаковка. Цель этого раздела — осветить основные аспекты системы модулей и импортирования Python, необходимые для структурирования ваших проектов. Далее мы рассмотрим разные подходы к сборке кода, который легко будет расширять и тестировать.

Благодаря тому, как в Python налажен процесс импортирования и разбиения на модули, структурировать проект довольно просто: существует всего несколько ограничений, модель для импортирования также нетрудно освоить. Поэтому перед вами стоит исключительно архитектурная задача — создать различные части проекта и продумать их взаимодействие.

Модули

Модуль — это один из основных уровней абстракции в Python. Уровни абстракции позволяют программисту разбивать код на части, которые содержат связанные данные и функциональность.

Например, если один уровень проекта предназначен для взаимодействия с пользователем, а другой обрабатывает данные на низком уровне, наиболее логичным способом разделения этих двух слоев является размещение всей функциональности, связанной со взаимодействием, в одном файле, а всех низкоуровневых операций — в другом. Такая группировка разметит их в два разных модуля. Файл для взаимодействия затем импортирует файл для низкоуровневой обработки с помощью выражения import module или from module import attribute.

Как только вы пустите в ход выражение import, вы начнете пользоваться модулями. Модули могут быть либо встроенными (вроде os и sys), либо сторонними пакетами, установленными в среде (вроде Requests или NumPy), либо внутренними модулями проекта.

Далее показан пример некоторых выражений import (подтверждается, что импортированный модуль является объектом Python со своим типом данных):

>>> import sys  # built-in module

>>> import matplotlib.pyplot as plt  # сторонний модуль

>>>

>>> import mymodule as mod  # внутренний модуль проекта

>>>

>>> print(type(sys), type(plt), type(mod))

<class 'module'> <class 'module'> <class 'module'>

В соответствии с руководством по стилю кода (/) присваивайте модулям короткие имена, которые начинаются со строчной буквы. И убедитесь, что не использовали специальные символы вроде точки (.) или вопросительного знака (?), поскольку это может нарушить вид Python для модулей. Поэтому вам следует избегать имен файла вроде my.spam.py (Python попытается найти файл spam.py в каталоге с именем my, а это неверно). В документации Python () более подробно описывается нотация с точкой.

Импортирование модулей. Помимо следования некоторым ограничениям в именовании, для использования файла Python в качестве модуля не требуется больше ничего особенного. Однако понимать механизм импортирования будет нелишним. Во-первых, выражение import modu начнет искать определение modu в файле с именем modu.py в том же каталоге, где находится и вызывающая сторона, если такой файл существует. При неудаче интерпретатор Python будет рекурсивно искать файл modu.py в пути поиска Python () и сгенерирует исключение ImportError, если не найдет. Путь поиска зависит от платформы и включает в себя определенные пользователем или системой каталоги, указанные в переменной среды $PYTHONPATH (или %PYTHONPATH% в Windows). Ее можно просмотреть или изменить в сессии Python:

import sys

>>> sys.path

[ '', '/current/absolute/path', 'etc']

# Реальный список содержит каждый путь, где выполняется поиск,

# когда вы импортируете библиотеки в Python в том порядке,

# в котором они проверяются.

Как только файл modu.py будет найден, интерпретатор Python запустит модуль в ограниченной области видимости. Любое выражение верхнего уровня в файле modu.py будет выполнено, включая другие выражения импорта, если таковые существуют. Определения функций и классов хранятся в словаре модуля. Наконец, переменные функции и классы модуля будут доступны вызывающей стороне с помощью пространства имен модуля — основной концепции программирования, которая особенно эффективна в Python. Пространства имен предоставляют область видимости, содержащую именованные атрибуты, которые видны друг другу, но к ним нельзя получить доступ из-за пределов пространства имен.

Во многих языках директива заставляет препроцессор, по сути, скопировать содержимое включаемого файла в код вызывающей стороны. В Python все происходит иначе: включаемый код изолируется в пространстве имен модуля. Результатом выполнения выражения import modu станет объект модуля с именем modu, который будет находиться в глобальном пространстве имен, его атрибуты будут доступны с помощью точечной нотации. Например modu.sqrt — это объект sqrt, определенный внутри файла modu.py. Это означает, что вам, как правило, не нужно волноваться о том, что включаемый код может делать что-то нежелательное, к примеру переопределять существующую функцию с тем же именем.

Инструменты для пространств имен

Функции dir(), globals() и locals() помогают быстро исследовать пространства имен:

dir(object) возвращает список атрибутов, к которым объект может получить доступ;

globals() возвращает словарь атрибутов, находящихся в данный момент в глобальном пространстве имен, а также их значения;

locals() возвращает словарь атрибутов в текущем локальном пространстве имен (например, внутри функции), а также их значения.

Для получения более подробной информации обратитесь к разделу Data model официальной документации Python ().

Вы можете симулировать более привычное поведение, используя специальный синтаксис в выражении import: from modu import *. Однако это, как правило, считается признаком плохого тона: наличие конструкции import * усложняет чтение кода, делает зависимости более связанными и может затереть (перезаписать) существующие определенные объекты новыми описаниями из импортированного модуля.

Нотация from modu import func — это способ импортировать только необходимые вам атрибуты в глобальное пространство имен. Она гораздо безопаснее нотации from modu import *, поскольку явно показывает, что именно импортируется в глобальное пространство имен. Единственное ее преимущество перед более простой нотацией import modu в том, что она сэкономит вам немного времени.

В табл. 4.1 сравниваются разные способы импортирования определений из других модулей.

Таблица 4.1. Разные способы импортировать определения из модулей

Очень плохой код  (непонятный  для читателя)

Код получше (здесь  понятно, какие имена  находятся в глобальном пространстве имен)

Лучший код  (сразу понятно, откуда появился тот или иной атрибут)

from modu import *

from modu import sqrt

import modu

x = sqrt(4)

x = sqrt(4)

x = modu.sqrt(4)

from modu import sqrt

from modu import sqrt

from modu import sqrt

Как упоминается в разделе «Стиль кода» в начале этой главы, читаемость — одна из основных особенностей Python. Читаемый код не содержит бесполезного текста. Но не следует максимально его сокращать в угоду краткости. Явно указывая, откуда появился тот или иной класс или функция, как в случае идиомы modu.func(), вы повышаете читаемость кода и степень его понимания.

Структура — это главное

Несмотря на то что вы можете структурировать проект так, как вам нравится, следует избегать некоторых ошибок.

Большое количество запутанных циклических зависимостей. Если для ваших классов Table и Chair из файла furn.py нужно импортировать класс Carpenter из файла workers.py (чтобы ответить на вопрос table.is_done_by() («произведены кем?»)) и если для класса Carpenter нужно импортировать классы Table и Chair (чтобы ответить на вопрос carpenter.what_do() («что производит?»)), у вас имеется циклическая зависимость: файл furn.py зависит от файла workers.py, который зависит от файла furn.py. В таком случае вам нужно использовать выражение import внутри методов, дабы избежать исключения ImportError.

Скрытое связывание. После каждого изменения в реализации класса Table вдруг перестают работать 20 несвязанных с ним тестов, поскольку это нарушает реализацию класса Carpenter. Это требует проведения аккуратных изменений для того, чтобы к ним адаптироваться, и означает, что в своем коде класса Carpenter вы делаете слишком много предположений о классе Table.

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

Спагетти-код. Вложенные условия if, расположенные на нескольких страницах подряд, и циклы for, содержащие большое количество скопированного

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

Равиоли-код. Такой код в Python встретить более вероятно, чем спагетти-код. Равиоли-код состоит из сотен небольших логических фрагментов, зачастую классов или объектов, которые не имеют хорошей структуры. Если вы не можете вспомнить, нужны ли вам для выполнения текущей задачи классы FurnitureTable, AssetTable, Table или даже TableNew, то, скорее всего, работаете с равиоли-кодом.

Упаковка

Python предоставляет довольно понятную систему упаковки, которая расширяет механизм модулей так, что он начинает работать с каталогами.

Любой каталог, содержащий файл __init__.py, считается пакетом Python. Каталог высшего уровня, в котором находится файл __init__.py, является корневым пакетом. Разные модули пакетов импортируются аналогично простым модулям, но файл __init__.py при этом будет использован для сбора всех описаний на уровне пакета.

Файл modu.py, находящийся в каталоге pack/, импортируется с помощью выражения import pack.modu. Интерпретатор выполнит поиск файла __init__.py в pack и запустит все его выражения верхнего уровня. Затем выполнит поиск файла с именем pack/modu.py и запустит все его выражения верхнего уровня. После этих операций любая переменная, функция или класс, определенные в файле modu.py, будут доступны пространству имен pack.modu.

Распространенная проблема заключается в том, что файлы __init__.py содержат слишком много кода. Когда сложность проекта повышается, в структуре каталогов могут появляться подпакеты и подподпакеты. В этом случае импортирование одного элемента из подподпакета потребует запуска всех файлов __init__.py, встреченных в дереве на пути к искомому.

Признаком хорошего тона является поддержание файла __init__.py пустым, когда модули и подпакеты пакета не имеют общего кода. Проекты HowDoI и Diamond, использованные в качестве примеров в следующем разделе, не содержат кода в файлах __init__.py, помимо номеров версий. В проектах Tablib, Requests и Flask в этом файле есть строка документации верхнего уровня и выражения импорта, предоставляющие API каждого проекта. Проект Werkzeug также предоставляет API верхнего уровня, но делает это с помощью ленивой загрузки (дополнительного кода, который добавляет содержимое в пространство имен, только когда тот используется, что ускоряет работу исходного выражения импорта).

Наконец, для импортирования глубоких вложенных пакетов доступен удобный синтаксис: import very.deep.module as mod. Это позволяет использовать слово mod на месте избыточной конструкции very.deep.module.

Объектно-ориентированное программирование

Python иногда описывается как объектно-ориентированный язык. Это может вносить путаницу, поэтому давайте проясним данный вопрос.

В Python все элементы являются объектами и могут быть обработаны как объекты. Именно это мы имеем в виду, когда говорим, что функции являются объектами первого класса. Функции, классы, строки и даже типы считаются в Python объектами: все они имеют тип, их можно передать как аргументы функций, они могут иметь методы и свойства. С этой точки зрения Python действительно объектно-ориентированный язык.

Однако, в отличие от Java, в Python парадигма объектно-ориентированного программирования не будет основной. Проект, написанный на Python, вполне может быть не объектно-ориентированным, то есть в нем не будут использоваться (или будут, но в небольших количествах) определения классов, наследование классов или другие механизмы, характерные для объектно-ориентированного программирования. Для питонистов эта функциональность доступна, но необязательна. Более того, как вы могли увидеть в подразделе «Модули» текущего раздела, способ, с помощью которого Python обрабатывает модули и пространства имен, дает разработчику возможность гарантировать инкапсуляцию и разделение между абстрактными уровнями — наиболее распространенную причину использования парадигмы объектно-ориентированного программирования — без наличия классов.

Защитники функционального программирования (парадигма, которая в своей чистейшей форме не имеет операторов присваивания и побочных эффектов и вызывает функции одну за другой, чтобы выполнить задачу) могут утверждать: из-за того, что функция выполняет разную работу в зависимости от состояния системы (например, от глобальной переменной, которая указывает, вошел ли пользователь под своей учетной записью), могут возникать ошибки и путаница. В Python (несмотря на то что он не является чисто функциональным языком) имеются инструменты, которые позволяют заниматься функциональным программированием (). Мы можем ограничить применение пользовательских классов до ситуаций, когда понадобится объединить состояние и функциональность.

В некоторых архитектурах, обычно в веб-приложениях, создается несколько процессов Python для того, чтобы реагировать на внешние запросы, которые могут происходить одновременно. В этом случае сохранение состояния созданных объектов (означает хранение статичной информации о мире) может привести к состоянию гонки. Этот термин употребляется при описании ситуации, когда в какой-то момент между инициализацией состояния объекта (которая в Python выполняется с помощью метода Class.__init__()) и использованием его состояния с помощью одного из методов состояние мира изменилось.

Например, запрос может загрузить предмет в память и затем пометить, что он добавлен в корзину пользователя. Если другой запрос в то же время «продаст» такой же предмет другому человеку, может случиться, что продажа на самом деле произойдет после того, как первая сессия добавит предмет (затем мы попытаемся продать предмет, который уже помечен как проданный). Подобные проблемы приводят к тому, что многие предпочитают функции, не сохраняющие состояние.

Мы дадим следующую рекомендацию: при работе с кодом, полагающимся на некий устойчивый контекст или глобальное состояние (как и многие веб-приложения), используйте функции и процедуры, которые привнесут минимальное количество неявных контекстов и побочных эффектов. Неявный контекст функции создается из любых глобальных переменных и элементов на уровне сохраняемости, к которым можно получить доступ из функции. Побочные эффекты — это изменения, которые функция вносит в свой неявный контекст. Если функция сохраняет или удаляет данные в глобальной переменной или на уровне сохраняемости, можно сказать, что она имеет побочные эффекты.

Пользовательские классы в Python необходимо применять для того, чтобы аккуратно изолировать функции, имеющие контексты и побочные эффекты, от функций, которые имеют логику (называются чистыми функциями). Чистые функции всегда определены: учитывая фиксированные входные данные, результат их работы неизменен, потому что они не зависят от контекста и не имеют побочных эффектов. Функция print(), например, не является чистой, поскольку ничего не возвращает, а записывает данные в стандартный поток ввода-вывода как побочный эффект.

Рассмотрим преимущества чистых функций:

их проще изменить или заменить, если нужно выполнить рефакторинг;

• их проще тестировать с помощью юнит-тестов, не нужно выполнять сложную настройку контекста и очищать данные после ее работы;

ими проще манипулировать, их легче декорировать (к этой теме мы сейчас вернемся) и передавать.

В итоге для некоторых инфраструктур чистые функции выступают более эффективными строительными блоками, чем классы или объекты, поскольку не имеют контекста и побочных эффектов. В качестве примера рассмотрим функции ввода-вывода, связанные с каждым форматом файла в библиотеке Tablib (tablib/formats/*.py — мы опишем Tablib в следующей главе). Они являются чистыми функциями, а не частью класса, поскольку лишь считывают данные из отдельного объекта типа Dataset, в котором хранятся, либо записывают объект типа Dataset в файл. Но объект типа Session в библиотеке Requests (ее мы также рассмотрим в следующей главе) — это класс, поскольку он должен сохранять cookies и информацию об аутентификации, которая может пригодиться при обмене данными в ходе сессии HTTP.

10685.png

Объектно-ориентированное программирование — полезная и даже необходимая парадигма программирования во многих случаях, например при разработке графических приложений для десктопа или игр, где вы можете манипулировать объектами (окнами, кнопками, аватарами, машинами), которые долго живут в памяти компьютера; является одной из причин использовать объектно-реляционное отображение, которое соотносит строки базы данных с объектами в коде. Этот вопрос рассматривается в разделе «Библиотеки для работы с базами данных» главы 11.

Декораторы

Декораторы были добавлены в Python в версии 2.4, определены и рассмотрены в PEP 318 (/). Декоратор — это функция или метод класса, которые оборачивают (или декорируют) другую функцию или метод. Декорированная функция или метод заменят оригинал. Поскольку функции являются объектами первого класса в Python, декорирование можно выполнить вручную, но все же более предпочтителен синтаксис @decorator. Рассмотрим пример использования декоратора:

>>> def foo():

...     print("I am inside foo.")

...

...

...

>>> import logging

>>> logging.basicConfig()

>>>

>>> def logged(func, *args, **kwargs):

...     logger = logging.getLogger()

...     def new_func(*args, **kwargs):

...         logger.debug("calling {} with args {} and kwargs {}".format(

...                      func.__name__, args, kwargs))

...         return func(*args, **kwargs)

...     return new_func

...

>>>

>>>

... @logged

... def bar():

...     print("I am inside bar.")

...

>>> logging.getLogger().setLevel(logging.DEBUG)

>>> bar()

DEBUG:root:calling bar with args () and kwargs {}

I am inside bar.

>>> foo()

I am inside foo.

Этот механизм подойдет, чтобы изолировать основную логику функции или метода. Примером задачи, для которой нужно использовать декорирование, можно назвать запоминание или кэширование: вы хотите сохранить результат дорогой функции в таблице и использовать его вместо того, чтобы выполнять повторные вычисления. Очевидно, это не является частью логики функции. В PEP 3129 (/), начиная с Python 3, декораторы также можно применять к классам.

Динамическая типизация

Python — динамически типизированный язык (в противоположность статически типизированным). Это означает, что переменные не имеют фиксированного типа. Переменные реализуются как указатели на объект, что дает возможность задать сначала значение 42, затем значение thanks for all the fish, а потом установить в качестве значения функцию.

Динамическая типизация, используемая в Python, зачастую считается недостатком, поскольку может привести к сложностям и появлению кода, для которого сложно выполнять отладку: если именованный объект может иметь в качестве значения множество разных вещей, разработчик поддерживающий код, должен отслеживать это имя в коде, чтобы убедиться, что оно не получило неуместное значение. В табл. 4.2 перечислены правила хорошего и плохого тона при именовании.

Таблица 4.2. Правила хорошего и плохого тона при задании имен

Совет

Плохой код

Хороший код

Используйте короткие функции или методы, чтобы снизить риск указания одного имени для двух несвязанных объектов

a = 1

a = 'answer is {}'.format(a)

def get_answer(a):

    return 'answer is

    {}'.format(a)

a = get_answer(1)

Используйте разные имена для связанных элементов, если они имеют разные типы

# Строка ...

items = 'a b c d'

# А теперь список

items = items.split(' ')

# А теперь множество

items = set(items)

items_string = 'a b c d'

items_list = items.split(' ')

items = set(items_list)

Повторное использование имен не повышает эффективность: операция присваивания все равно создаст новый объект. При росте сложности, когда операции присваивания разделены другими строками кода, сложно определить тип переменной.

В некоторых видах программирования, включая функциональное, не рекомендуется пользоваться возможностью повторного присваивания значения переменным. В Java вы можете указать, что переменная всегда будет содержать одно и то же значение после присваивания, с помощью ключевого слова final. В Python такого ключевого слова нет (это шло бы вразрез с его философией). Но присваивание значения переменной всего один раз может быть признаком дисциплинированности. Это помогает поддержать концепцию изменяемых и неизменяемых типов.

10712.png

Pylint (/) предупредит вас, если вы попытаетесь присвоить переменной, уже содержащей значение одного типа, значение другого типа.

Изменяемые и неизменяемые типы

В Python имеются два типа встроенных или определяемых пользователем типов:

# Списки можно изменять

my_list = [1, 2, 3]

my_list[0] = 4

print my_list  # [4, 2, 3] <- тот же список, измененный.

# Целые числа изменять нельзя

x = 6

x = x + 1  # Новое значение x занимает другое место в памяти.

Изменяемые типы. Позволяют изменять содержимое объекта на месте. Примерами могут стать списки и словари, которые имеют изменяющие методы вроде list.append() или dict.pop() и могут быть модифицированы на месте.

Неизменяемые типы. Не предоставляют методов для изменения их содержимого. Например, переменная х со значением 6 не имеет метода для инкремента. Для того чтобы вычислить значение выражения х + 1, нужно создать другую целочисленную переменную и дать ей имя.

Одно из последствий такого поведения — объекты изменяемых типов не могут быть использованы как ключи для словаря, ведь если их значение изменится, то изменится и его хэш (словари используют хэширование для хранения ключей). Неизменяемым эквивалентом списка является кортеж. Он создается добавлением круглых скобок, например (1, 2). Кортеж нельзя изменить на месте, поэтому его можно использовать как ключ словаря.

Правильное применение изменяемых типов для объектов, которые по задумке должны изменяться (например, my_list = [1, 2, 3]), и неизменяемых типов для объектов, которые по задумке должны иметь фиксированное значение (например, islington_phone = ("220", "7946", "0347")), поможет другим разработчикам понять код.

В Python строки неизменяемы и это может удивить новичков. Попытка изменить строку вызовет ошибку:

>>> s = "I'm not mutable"

>>> s[1:7] = " am"

Traceback (most recent call last):

  File "<stdin>", line 1, in <module>

TypeError: 'str' object does not support item assignment

Это означает, что при создании строки по частям гораздо эффективнее собрать все части в список, поскольку его можно изменять, а затем объединить их. Кроме того, в Python предусмотрены списковые включения, которые предоставляют простой синтаксис для итерирования по входным данным для создания списка. В табл. 4.3 приведены способы создания строки из итерабельного объекта.

Таблица 4.3. Способы конкатенации строки

Плохой

Хороший

Лучший

>>> s = ""

>>> for c in (97, 98, 98):

...    s += unichr(c)

...

>>> print(s)

abc

>>> s = []

>>> for c in (97, 98, 99):

...    s.append(unichr(c))

...

>>> print("".join(s))

abc

>>> r = (97, 98, 99)

>>> s = [unichr(c) for

    c in r]

>>> print("".join(s))

abc

На главной странице Python (/) вы можете найти обсуждение подобной оптимизации.

Наконец, если количество элементов конкатенации известно, добавить строку будет проще (и очевиднее), чем создавать список элементов только для того, чтобы вызвать функцию "".join().

Все следующие варианты форматирования для определения переменной cheese делают одно и то же:

>>> adj = "Red"

>>> noun = "Leicester"

>>>

>>> cheese = "%s %s" % (adj, noun)  # Этот стиль устарел (PEP 3101)

>>> cheese = "{} {}".format(adj, noun)  # Возможно начиная с Python 3.1

>>> cheese = "{0} {1}".format(adj, noun)  # Числа можно использовать повторно

>>> cheese = "{adj} {noun}".format(adj=adj, noun=noun)  # Этот стиль — лучший

>>> print(cheese)

Red Leicester

Зависимости, получаемые от третьей стороны

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

Однако можно достичь консенсуса: почти во всех случаях лучше всего держать зависимости отдельно друг от друга, поскольку это добавляет ненужное содержимое (зачастую мегабайты дополнительного кода) в репозиторий. Виртуальные среды, использованные в сочетании с файлами setup.py (предпочтительно, особенно если пакет является библиотекой) или requirements.txt (при использовании переопределит зависимости в файле setup.py в случае конфликтов), могут ограничить зависимости набором рабочих версий.

Если этих вариантов недостаточно, можно связаться с владельцем зависимости, чтобы решить проблему, обновив его пакет (например, ваша библиотека может зависеть от выходящего релиза его пакета или вам нужна новая функциональность). Эти изменения, скорее всего, пойдут на пользу всему сообществу. Однако здесь имеется и подводный камень: если вы отправите запрос на включение больших изменений, вам, возможно, придется поддерживать эти изменения по мере появления дальнейших предложений и запросов (по этой причине в проектах Tablib и Requests несколько зависимостей получены от третьей стороны). По мере полного перехода сообщества на Python 3 мы надеемся, что проблемных областей станет меньше.

Тестирование вашего кода

Тестировать код очень важно. Ведь люди будут использовать только такой проект, который на самом деле работает.

Модули doctest и unittest впервые появились в версии Python 2.1 (выпущена в 2001 году), поддерживая разработку через тестирование (test-driven development, TDD): разработчик сначала пишет тесты, которые определяют основную задачу и узкие места функции, а затем — функцию, которая проходит эти тесты. С тех пор TDD стали чаще использовать в бизнес-проектах и проектах с открытым исходным кодом — практиковаться в написании кода теста и параллельно самой функции довольно полезно. Если пользоваться этим методом с умом, он поможет вам четко определить предназначение своего кода и создать развернутую модульную структуру.

Советы по тестированию

Тест — это самый объемный фрагмент кода, который автостопщик может написать. Приведем несколько советов.

Тестируйте что-то одно за раз. Юнит-тест должен концентрироваться на небольшом фрагменте функциональности и доказывать, что все работает, как требуется.

Независимость императивна. Каждый юнит-тест должен быть полностью независимым: его можно запустить как отдельно, так и внутри набора тестов без учета того, в каком порядке они вызываются. Из этого правила следует, что для каждого теста нужно загрузить свежий набор данных, а после его выполнения провести очистку (обычно с помощью методов setUp() и tearDown()).

Точность лучше простоты. Используйте длинные описательные имена для функций теста. Это правило отличается от правила для рабочего кода, где предпочительны короткие имена. Причина в том, что функции никогда не вызываются явно. В рабочем коде допускается использование имен square() или даже sqr(), но в коде теста у вас должны быть имена вроде test_square_of_number_2() или test_square_negative_number(). Эти имена функций будут выведены, когда тест даст сбой, они должны быть максимально описательными.

Скорость имеет значение. Старайтесь писать тесты, которые работают быстро. Если для того, чтобы тест отработал, нужно несколько миллисекунд, разработка будет замедлена или тесты будут запускаться не так часто, как вам бы этого хотелось. В некоторых случаях тесты не могут быть быстрыми, поскольку для их работы требуется сложная структура данных, которая должна подгружаться каждый раз, когда запускается тест. Держите подобные тесты в отдельном наборе, который запускается какой-нибудь задачей по графику, а остальные тесты запускайте так часто, как вам это нужно.

RTMF (Read the manual, friend! — «Читай руководство, друг!»). Изучайте свои инструменты, чтобы знать, как запустить отдельный тест или набор тестов. При разработке функции внутри модуля почаще запускайте тесты для нее, в идеале всякий раз, когда вы сохраняете код.

Тестируйте все в начале работы и затем опять тестируйте по ее завершении. Всегда запускайте полный набор тестов перед тем, как писать код, и по завершении работы. Это позволит убедиться, что вы ничего «не сломали» в остальной части кода.

Автоматические функции перехвата для системы управления версиями фантастически хороши. Реализовать функцию перехвата, которая запускает все тесты перед тем, как отправить код в общий репозиторий, — хорошая идея. Вы можете непосредственно добавлять функции перехвата в вашу систему контроля версий, некоторые IDE предоставляют способы сделать это с помощью их собственных сред. Далее приведены ссылки на документацию к популярным системам контроля версий, в которой содержится информация о том, как это реализовать:

GitHub (/);

• Mercurial ();

Subversion ().

Напишите тест, если хотите сделать перерыв. Если вы остановились на середине сессии разработки и вам нужно прервать работу, можете написать неработающий тест, который связан с тем, что вы планируете реализовать. По возвращении к работе у вас будет указатель на то место, где вы остановились (вы сможете приступить быстрее).

В случае неопределенности выполните отладку для теста. Первый шаг отладки кода — написание еще одного теста, который указывает на ошибку. Несмотря на то что это не всегда можно сделать, тесты, отлавливающие ошибки, являются наиболее ценными фрагментами кода вашего проекта.

Если тест сложно объяснить, то желаем вам удачи в поиске коллег. Если что-то идет не так или что-то нужно изменить и для вашего кода написано множество тестов, вы или другие сотрудники, работающие над проектом, будете полагаться на набор тестов для решения проблемы или изменения поведения. Поэтому код теста должен быть читаемым на том же уровне (или даже больше), чем рабочий код. Юнит-тест, чье предназначение неясно, не принесет большой пользы.

Если тест просто объяснить, он почти всегда хорош. Код теста можно использовать в качестве руководства для новых разработчиков. Если другим людям нужно работать с базой кода, запуск и чтение соответствующих тестов — это лучшее, что они могут сделать. Они обнаружат (по крайней мере должны обнаружить) проблемные места, вызывающие больше всего трудностей, а также пограничные случаи. Если им нужно добавить какую-то функциональность, в первую очередь следует добавить тест (это гарантирует ее появление).

Не паникуйте! Это же ПО с открытым исходным кодом! Вас поддержит весь мир.

Основы тестирования

В этом разделе приводятся основы тестирования, чтобы у вас было представление о доступных вариантах, и примеры из проектов Python, которые мы рассмотрим в главе 5. Есть целая книга, посвященная TDD в Python, мы не хотим переписывать ее здесь. Она называется Test-Driven Development with Python (издательство O’Reilly).

unittest

unittest — это тестовый модуль стандартной библиотеки Python, готовый к работе сразу после установки. Его API будет знаком всем, кто пользовался любым из этих инструментов — JUnit (Java)/nUnit (.NET)/CppUnit (C/C++).

Создать тест в этом модуле можно путем создания подкласса для unittest.TestCase. В этом примере функция тестирования определяется как новый метод в MyTest:

# test_example.py

import unittest

def fun(x):

    return x + 1

class MyTest(unittest.TestCase):

    def test_that_fun_adds_one(self):

        self.assertEqual(fun(3), 4)

class MySecondTest(unittest.TestCase):

    def test_that_fun_fails_when_not_adding_number(self):

        self.assertRaises(TypeError, fun, "multiply six by nine")

10749.png

Методы теста должны начинаться со строки test — иначе они не запустятся. Тестовые модули должны следовать шаблону test*.py по умолчанию, но могут соответствовать любому шаблону, который вы передадите с помощью аргумента с ключевым словом pattern в командной строке.

Для того чтобы запустить все тесты в TestClass, откройте терминальную оболочку. Находясь в том же каталоге, где файл, вызовите из командной строки модуль unittest:

$ python -m unittest test_example.MyTest

.

----------------------------------------------------------------------

Ran 1 test in 0.000s

OK

Для запуска всех тестов из файла укажите файл:

$ python -m unittest test_example

.

----------------------------------------------------------------------

Ran 2 tests in 0.000s

OK

Mock (в модуле unittest)

В версии Python 3.3 unittest.mock () доступен в стандартной библиотеке. Он позволяет заменять тестируемые части системы mock-объектами и делать предположения о том, как они используются.

Например, вы можете написать обезьяний патч для метода, похожий на тот, что показан в предыдущем примере (обезьяний патч — это код, который модифицирует или заменяет другой существующий код во время работы программы). В этом коде существующий метод с именем ProductionClass.method (в случае если мы создали именованный объект) заменяется новым объектом MagicMock, который при вызове всегда будет возвращать значение 3. Кроме того, этот объект считает количество получаемых вызовов, записывает сигнатуру, с помощью которой был вызван, и содержит методы с выражением, необходимые для тестов:

from unittest.mock import MagicMock

instance = ProductionClass()

instance.method = MagicMock(return_value=3)

instance.method(3, 4, 5, key='value')

instance.method.assert_called_with(3, 4, 5, key='value')

Для того чтобы создавать mock-классы и объекты при тестировании, используйте декоратор patch. В следующем примере поиск во внешней системе заменяется mock-объектом, который всегда возвращает одинаковый результат (патч существует только во время работы теста):

import unittest.mock as mock

def mock_search(self):

    class MockSearchQuerySet(SearchQuerySet):

        def __iter__(self):

            return iter(["foo", "bar", "baz"])

    return MockSearchQuerySet()

# SearchForm относится к ссылке на импортированный класс

# myapp.SearchForm и модифицирует этот объект, но не код,

# где определяется сам класс SearchForm

@mock.patch('myapp.SearchForm.search', mock_search)

def test_new_watchlist_activities(self):

    # get_search_results выполняет поиск и итерирует по результату

    self.assertEqual(len(myapp.get_search_results(q="fish")), 3)

Вы можете сконфигурировать модуль mock и управлять его поведением разными способами. Они подробно описаны в документации к unittest.mock.

doctest

Модуль doctest выполняет поиск фрагментов текста, которые похожи на интер­активные сессии Python в строках документации, а затем выполняет эти сессии, чтобы убедиться, что они работают именно так, как было показано.

Модуль doctest служит другой цели, нежели юнит-тесты. Они обычно менее детальны и не отлавливают особые случаи или регрессионные ошибки. Вместо этого они выступают в качестве содержательной документации основных вариантов использования модуля и его компонентов (в качестве примера можно рассмотреть сценарий «счастливый путь» (happy path — )). Однако такие тесты должны запускаться автоматически каждый раз, когда запускается весь набор тестов.

Рассмотрим простой пример doctest:

def square(x):

    """Squares x.

    >>> square(2)

    4

    >>> square(-2)

    4

    """

    return x * x if __name__ == '__main__':

    import doctest

    doctest.testmod()

Когда вы запускаете этот модуль из командной строки (например, с помощью команды python module.py), такие тесты начнут выполняться и «пожалуются», если какой-то компонент ведет себя не так, как описано в строках документации.

Примеры

В этом разделе мы рассмотрим фрагменты наших любимых пакетов для того, чтобы подчеркнуть правила хорошего тона при тестировании реального кода. Набор тестов предполагает наличие дополнительных библиотек, не включенных в эти пакеты (например, для Requests требуется Flask, чтобы создать mock-сервер HTTP), которые включены в файлы requirements.txt их проектов.

Для всех этих примеров ожидаемым первым шагом будет открытие терминальной оболочки, изменение каталогов таким образом, чтобы они указывали на то место, где лежат исходники к вашим проектам, а также клонирование репозитория исходного кода и настройка виртуальной среды. Например, так:

$ git clone

$ cd projectname

$ virtualenv -p python3 venv

$ source venv/bin/activate

(venv)$ pip install -r requirements.txt

Пример: тестирование в Tablib

Tablib использует модуль unittest стандартной библиотеки Python. Набор тестов не поставляется с пакетом. Для получения файлов вы должны клонировать репозиторий GitHub. Приводим основные моменты, выделив главные части.

10765.png 

10770.png 

10775.png Для того чтобы использовать юнит-тест, создайте подкласс unittest.TestCase и напишите методы для тестирования, чьи имена начинаются с test. Класс TestCase предоставляет методы с выражением, которые позволяют выполнить проверку на равенство, правдивость, тип данных и наличие исключений (см. документацию по адресу для получения более подробной информации).

10785.png Метод TestCase.setUp() запускается всякий раз перед каждым методом TestCase.

10793.pngМетод TestCase.tearDown() запускается всякий раз после каждого метода TestCase.

10805.png Все имена тестов должны начинаться со слова test, иначе они не запустятся.

10816.png В одном тестовом случае может быть несколько тестов, но каждый из них должен тестировать что-то одно.

Если вы хотите внести вклад в Tablib, первое, что можете сделать после клонирования репозитория, — запустить набор тестов и убедиться, что все работает как полагается. Это можно сделать так:

(venv)$ ### внутри каталога высшего уровня, tablib/

(venv)$ python -m unittest  test_tablib.py

..............................................................

----------------------------------------------------------------------

Ran 62 tests in 0.289s

OK

В версии Python 2.7 метод unittest также содержит собственный механизм обнаружения тестов, который доступен с помощью параметра discover в командной строке:

(venv)$ ### *above* the top-level directory, tablib/

(venv)$ python -m unittest discover tablib/

..............................................................

----------------------------------------------------------------------

Ran 62 tests in 0.234s

OK

После того как вы убедитесь, что все тесты проходят, вы: а) найдете тестовый случай, связанный с изменяемой частью проекта, и будете часто запускать его при изменении кода; б) напишете новый тестовый случай для функциональности, которую хотите добавить, или для ошибки, которую отслеживаете, и будете часто запускать его при изменении кода. Рассмотрим в качестве примера следующий сниппет:

(venv)$ ### внутри каталога высшего уровня, tablib/

(venv)$ python -m unittest test_tablib.TablibTestCase.test_empty_append

.

----------------------------------------------------------------------

Ran 1 test in 0.001s

OK

Как только ваш код начнет работать, снова задействуйте весь набор тестов перед тем, как отправить его в репозиторий. Поскольку вы часто запускаете тесты, они должны быть максимально быстрыми. Более подробную информацию о том, как использовать метод unittest, смотрите в документации по адресу .

Пример: тестирование с помощью Requests

Пакет Requests использует py.test. Чтобы увидеть его в действии, откройте терминальную оболочку, перейдите во временный каталог, клонируйте Requests, установите все зависимости и запустите файл py.test, как показано здесь:

$ git clone -q

$

$ virtualenv venv -q -p python3  # dash -q for 'quiet'

$ source venv/bin/activate

(venv)$

(venv)$ pip install -q -r requests/requirements.txt   # 'quiet' again...

(venv)$ cd requests

(venv)$ py.test

========================= test session starts =================================

platform darwin -- Python 3.4.3, pytest-2.8.1, py-1.4.30, pluggy-0.3.1

rootdir: /tmp/requests, inifile:

plugins: cov-2.1.0,

collected 219 items

tests/test_requests.py ........................................................

X............................................

tests/test_utils.py ..s....................................................

========= 217 passed, 1 skipped, 1 xpassed in 25.75 seconds ===================

Другие популярные инструменты

Инструменты для тестирования, перечисленные здесь, используются не так часто, но все еще достаточно популярны.

pytest

pytest (/) — это нешаблонная альтернатива модуля стандартной библиотеки Python. Это означает, что для него не требуется создавать временные платформы для тестовых случаев и, возможно, даже не нужны методы установки и очистки. Для установки запустите команду pip в обычном режиме:

$ pip install pytest

Несмотря на то что инструмент тестирования имеет множество возможностей и его можно расширять, синтаксис остается довольно простым. Создать набор тестов так же просто, как и написать модуль с несколькими функциями:

# содержимое файла test_sample.py

def func(x):

    return x + 1

def test_answer():

    assert func(3) == 5

После этого вам лишь нужно вызвать команду py.test. Сравните это с работой, которая потребуется для создания эквивалентной функциональности с помощью модуля unittest:

$ py.test

=========================== test session starts ============================

platform darwin -- Python 2.7.1 -- pytest-2.2.1

collecting ... collected 1 items

test_sample.py F

================================= FAILURES =================================

_______________________________ test_answer ________________________________

    def test_answer():

>       assert func(3) == 5

E       assert 4 == 5

E        +  where 4 = func(3)

test_sample.py:5: AssertionError

========================= 1 failed in 0.02 seconds =========================

Nose

Nose (/) расширяет unittest для того, чтобы упростить тестирование:

$ pip install nose

Предоставляет возможность автоматически обнаруживать тесты, чтобы сэконо­мить ваше время и избавить от необходимости создавать наборы тестов вручную. Предлагает множество надстроек для дополнительной функциональности вроде совместимого с xUnit вывода тестов, отчетов о покрытии, а также выбора тестов.

tox

tox (/) — инструмент для автоматизирования управления средами тестирования и для тестирования в разных конфигурациях интерпретатора:

$ pip install tox

tox позволяет сконфигурировать сложные матрицы тестов с большим количеством параметров с помощью конфигурационного файла, похожего на INI-файлы.

Варианты для старых версий Python

Если вы не можете контролировать свою версию Python, но хотите использовать эти инструменты тестирования, предлагаем вам несколько вариантов.

unittest2. Это обратный порт модуля unittest () для версии Python 2.7, который имеет усовершенствованный API и лучшие выражения относительно тех, что были доступны в предыдущих версиях Python.

Если вы используете Python 2.6 или ниже (например, если вы работаете в крупном банке или компании Fortune 500), можете установить его с помощью команды pip:

$ pip install unittest2

Вы можете захотеть импортировать модуль под именем unittest, чтобы вам было проще портировать код на новые версии модуля в будущем:

import unittest2 as unittest

class MyTest(unittest.TestCase):

    ...

Таким образом, если вы когда-нибудь перейдете на новую версию Python и вам больше не потребуется модуль unittest2, вы сможете изменить выражение импорта, не меняя остальной код.

Mock. Если вам понравилось то, что вы прочитали в пункте «Mock (в модуле unittest)» раздела «Основы тестирования» выше, но вы работаете с Python в версии ниже 3.3, вы все еще можете использовать unittest.mock, импортировав его как отдельную библиотеку:

$ pip install mock

fixture. Предоставляет инструменты, которые позволяют проще настраивать и очищать бэкенды баз данных для тестирования (/). Он может загружать фальшивые наборы данных для использования в SQLAlchemy, SQLObject, Google Datastore, Django ORM и Storm. Существуют и его новые версии, но его тестировали только для версий Python 2.4-2.6.

Lettuce и Behave

Lettuce и Behave — это пакеты для выполнения разработки через реализацию поведения (behavior-driven development, BDD) в Python. BDD — процесс, который появился на основе TDD в начале 2000-х годов для того, чтобы заменить слово «тест» в TDD на слово «поведение» (дабы преодолеть проблемы, возникающие у новичков при освоении TDD). Это название появилось благодаря Дэну Норту (Dan North) в 2003-м и было представлено миру наряду с инструментом JBehave для Java в статье 2006 года в журнале Better Socware (представляла собой отредактированную статью из блога Дэна Норта Introducing BDD по адресу ).

Концепция BDD набрала популярность после того, как в 2011 году вышла книга 6e Cucumber Book (Pragmatic Bookshelf), где был задокументирован пакет Behave для Ruby. Это вдохновило Гэбриэла Фалько (Gabriel Falco) на создание Lettuce (/), а Питера Паренте (Peter Parente) — на создание Behave (/) для нашего сообщества.

Поведения описываются простым текстом с помощью синтаксиса под названием Gherkin, который люди могут прочитать, а машины — понять. Вам могут пригодиться следующие руководства:

руководство по Gherkin ();

• руководство по Lettuce ();

руководство по Behave ().

Документация

Читаемость — главная цель разработчиков Python как в проектах, так и в документации. Приемы, описанные в этом разделе, помогут вам сэкономить немало времени.

Документация к проекту

Существует документация по API, предназначенная пользователям проектов, а также дополнительная документация для тех, кто хочет вносить в проект свой вклад. В этом разделе вы узнаете о дополнительной документации.

Файл README, расположенный в корневом каталоге, призван давать общую информацию как пользователям, так и тем, кто обслуживает проект. В нем должен быть либо простой текст, либо легкая для чтения разметка вроде reStructured Text (сейчас это единственный формат, который понимает PyPI) или Markdown (/). Этот файл должен содержать несколько строк, описывающих предназначение проекта или библиотеки (предполагая, что пользователь ничего не знает о проекте), URL основного исходного кода ПО и информацию об авторах. Если вы планируете читать код, то в первую очередь должны ознакомиться с этим файлом.

Файл INSTALL не особенно нужен в Python (но он может пригодиться для того, чтобы соответствовать требованиям лицензий вроде GPL). Инструкции по установке зачастую сокращаются до одной команды вроде pip install module или python setup.py install и добавляются в файл README.

Файл LICENSE должен присутствовать всегда и указывать лицензию, под которой ПО доступно общественности (см. раздел «Выбираем лицензию» далее в этой главе для получения более подробной информации.)

В файле TODO или одноименном разделе файла README должны быть представлены планы по развитию кода.

В файле CHANGELOG или одноименном разделе файла README должны быть приведены изменения, которые произошли с базой кода в последних версиях.

Публикация проекта

В зависимости от проекта ваша документация может содержать некоторые (или даже все) из этих компонентов:

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

• в разделе «Руководство» основные варианты использования описаны более подробно. Читатель пройдет пошаговую процедуру настройки рабочего прототипа;

• раздел API генерируется на основе кода (см. подраздел «Строки документации против блоковых комментариев» текущего раздела далее). В нем перечислены все доступные интерфейсы, параметры и возвращаемые значения;

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

Sphinx

Sphinx (/) — самый популярный инструмент для создания документации для Python. Используйте его: он преобразует язык разметки reStructured Text в огромное множество форматов, включая HTML, LaTeX (для печатаемых версий PDF), страницы руководства и простой текст.

Существует отличный бесплатный хостинг для вашей документации, созданной с помощью Sphinx: Read the Docs (/). Используйте и его. Вы можете сконфигурировать его с помощью функций перехвата коммитов для вашего репозитория исходного кода, поэтому перестроение вашей документации будет происходить автоматически.

10841.png

Sphinx знаменит благодаря генерации API. Он также хорошо работает для общей документации в проекте. Онлайн-версия книги «Автостопом по Python» создана с помощью Sphinx и размещена на сайте Read the Docs.

reStructured Text

Sphinx использует формат reStructured Text (), с его помощью написана практически вся документация для Python. Если содержимое аргумента long_description функции setuptools.setup() написано в формате reStructured Text, оно будет отрисовано как HTML в PyPI — другие форматы будут представлены как простой текст. Он похож на Markdown, име­ющий встроенные необязательные расширения. Следующие ресурсы подойдут для изучения синтаксиса:

The reStructuredText Primer ();

reStructuredText Quick Reference ().

Или начните вносить свой вклад в документацию к любимому проекту и учитесь в процессе чтения.

Строки документации против  блоковых комментариев

Строки документации и блоки комментариев не взаимозаменяемы. Оба варианта могут применяться для функции или класса. Рассмотрим пример использования обоих.

10855.png 

10862.png Первый блок комментария — это заметка для программиста.

10876.png Строки документации описывают, как работает функция или класс, она будет показана в интерактивной сессии Python, когда пользователь введет команду help(square_and_rooter).

Строки документации, размещенные в начале модуля или в начале файла __init__.py, также появятся в выводе функции help(). Функция autodoc для Sphinx может автоматически генерировать документацию с помощью правильно отформатированных строк. Инструкцию о том, как форматировать строки документации для autodoc, вы можете прочитать в руководстве к Sphinx (). Для получения более подробных сведений обратитесь к PEP 257 (/).

Журналирование

Модуль журналирования был частью стандартной библиотеки Python, начиная с версии 2.3. Он кратко описан в PEP 282 (/). Эту документацию сложно прочесть, исключение составляет простое руководство по журналированию ().

Журналирование бывает двух видов:

диагностическое журналирование — записываются все события, связанные с работой приложения. Если пользователь сообщает об ошибке, в этих журналах можно поискать контекст;

ведение контрольных журналов — записываются события для бизнес-анализа. Транзакции пользователя (вроде истории посещений) могут быть извлечены и объединены с другой информацией о нем (вроде итоговых покупок) для отчетности или оптимизации бизнес-целей.

Журналирование против функции print

Единственный случай, когда print предпочтительнее журналирования, — если вам нужно отобразить справку для приложения командной строки. Рассмотрим причины, почему журналирование лучше, чем print:

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

к событиям, записанным во включенных модулях, вы можете получить доступ автоматически с помощью корневого средства ведения журнала в потоке журналирования для вашего приложения, если только вы их не отфильтруете;

процесс журналирования можно выборочно приостанавливать с помощью метода logging.Logger.setLevel() или отключать путем установки значения атрибута logging.Logger.disabled равным True.

Журналирование для библиотеки

Заметки о конфигурировании журналирования для библиотеки содержатся в руководстве по журналированию (). Еще один хороший ресурс с примерами использования журналирования — библиотеки, которые мы упомянем в следующей главе. Поскольку пользователь (а не библиотека) должен указывать, что случится, когда произойдет событие журналирования, мы должны констатировать:

Настоятельно рекомендуется не добавлять никаких обработчиков помимо Null­Handler к средствам ведения журнала для библиотек.

Обработчик NullHandler делает то, что указано в его имени, то есть ничего. В противном случае пользователь должен будет самостоятельно отключать журналирование, если оно ему не требуется.

Правилом хорошего тона считается создание объектов средств ведения журнала только для использования вместе с переменной __name__ global: модуль журналирования создает иерархию средств ведения журнала с помощью точечной нотации, поэтому использование конструкции __name__ гарантирует отсутствие пересечений.

Рассмотрим пример применения этого приема в исходном коде библиотеки Requests () — разместите это в файле верхнего уровня __init__.py вашего проекта:

# Установить дескриптор журналирования по умолчанию для того, чтобы

# избежать появления предупреждений, которые гласят «Обработчик не найден».

import logging

try:  # Python 2.7+

    from logging import NullHandler

except ImportError:

    class NullHandler(logging.Handler):

        def emit(self, record):

            pass

logging.getLogger(__name__).addHandler(NullHandler())

Журналирование для приложения

Twelve-Factor App (/) (авторитетный источник, где перечислены правила хорошего тона, применяемые при разработке приложений) содержит раздел, в котором рассказывается о подобных правилах журналирования (). В нем предложено рассматривать события журнала как поток событий, для отправки этого потока в стандартный поток вывода нужно использовать среду приложения.

Существует минимум три способа конфигурирования средств ведения журнала (табл. 4.4).

Таблица 4.4. Способы конфигурирования средств ведения журнала

Способ

Плюсы

Минусы

Использование файла в формате INI

Вы можете обновлять конфигурацию при запуске функции logging.config.listen(), которая будет слушать изменения в сокете

У вас будет не такой полный конт­роль (например, пользовательские фильтры или средства ведения журнала, созданные как подклассы), чем это возможно при конфигурировании средств ведения журнала в коде

Использование словаря или файла в формате JSON

В дополнение к обновлению во время работы вы также можете загружать конфигурацию из файла с помощью модуля json, который находится в стандартной библиотеке, начиная с Python 2.6

У вас будет не такой полный контроль, чем это возможно при конфигурировании средств ведения журнала в коде

Использование кода

Вы имеете полный контроль над конфигурированием

Любые модификации потребуют внесения изменений в исходный код

Пример конфигурации с помощью файла в формате INI

Более подробная информация о формате INI содержится в разделе руководства журналирования, посвященном журналированию конфигурации (). Минимальный файл конфигурации будет выглядеть так:

[loggers]

keys=root

 

[handlers]

keys=stream_handler

 

[formatters]

keys=formatter

 

[logger_root]

level=DEBUG

handlers=stream_handler

 

[handler_stream_handler]

class=StreamHandler

level=DEBUG

formatter=formatter

args=(sys.stderr,)

 

[formatter_formatter]

format=%(asctime)s %(name)-12s %(levelname)-8s %(message)s

asctime, name, levelname и message являются необязательными атрибутами библиотеки журналирования. Полный список доступных вариантов и их описание смотрите в документации Pytho (). Предположим, что наша конфигурация журналирования называется logging_conf.ini. Для того чтобы настроить средства ведения журнала с помощью этой конфигурации в коде, используем функцию logging.config.fileconfig():

import logging

from logging.config import fileConfig

 

fileConfig('logging_config.ini')

logger = logging.getLogger()

logger.debug('often makes a very good meal of %s', 'visiting tourists')

Пример конфигурирования с помощью словаря

В версии Python 2.7 вы можете использовать словарь с деталями конфигурации. В PEP 391 () содержится список обязательных и необязательных элементов словаря конфигурации. Рассмотрим минимальную реализацию:

import logging

from logging.config dictConfig

 

logging_config = dict(

    version = 1,

    formatters = {

        'f' : {'format' :

                '%(asctime)s %(name)-12s %(levelname)-8s %(message)s' }

        },

    handlers = {

        'h': {'class': 'logging.StreamHandler',

                'formatter': 'f',

                'level': logging.DEBUG}

    loggers = {

        'root': {'handlers': ['h'],

                'level': logging.DEBUG}

        }

)

dictConfig(logging_config)

 

logger = debugging.getLogger()

logger.debug('often makes a very good meal of %s', 'visiting tourists')

Пример конфигурирования непосредственно в коде

Наконец, рассмотрим минимальную конфигурацию журналирования, расположенную непосредственно в коде:

import logging

 

logger = logging.getLogger()

handler = logging.StreamHandler()

formatter = logging.Formatter(

        '%(asctime)s %(name)-12s %(levelname)-8s %(message)s')

handler.setFormatter(formatter)

logger.addHandler(handler)

logger.setLevel(logging.DEBUG)

 

logger.debug('often makes a very good meal of %s', 'visiting tourists')

Выбираем лицензию

В Соединенных Штатах Америки, если для вашего исходного кода не указана лицензия, пользователи не получат законного права загружать, модифицировать или распространять его. Помимо этого, они не смогут вносить свой вклад в проект, если вы не укажете, по каким правилам играть. Поэтому вам нужна лицензия.

Лицензии

Если ваш проект основан на другом проекте, вам следует выбрать лицензию. Например, Python Software Foundation (PSF) просит всех, кто вносит свой вклад в исходный код, подписать соглашение для участников, которое формально лицензирует их код под одной из двух лицензий PSF (при этом они сохраняют авторские права). Поскольку обе лицензии позволяют сублицензировать код с другими условиями, PSF может свободно распространять Python под своей лицензией Python Software Foundation License. Вы можете ознакомиться с часто задаваемыми вопросами о лицензии PSF () (простым языком описывается, что пользователи могут и не могут делать). При этом дальнейшее применение дистрибутивов Python от PSF за пределами лицензии не предполагается.

Доступные варианты

Существует множество лицензий. PSF рекомендует использовать лицензию, одоб­ренную Open Source Institute (OSI) (). Если вы хотите вносить свой код в PSF, вам будет проще это делать, начав работу с одной из лицензий, указанных на странице /.

10919.png

Помните, что необходимо изменять текст заполнителя в шаблонах лицензий для того, чтобы добавить актуальную информацию. Например, шаблон для лицензии MIT содержит текст Copyright (c) <year> <copyright holders> во второй строке. Шаблон для лицензии Apache License, Version 2.0 не требует модификации.

Лицензии для открытого исходного кода, как правило, делятся на две категории.

• Разрешительные. Часто называются похожими на Berkeley Software Distribution (BSD), больше концентрируются на том, чтобы дать пользователю свободу относительно того, что он хочет сделать со своим ПО.

Примеры:

• лицензия Apache 2.0 () — это действующая лицензия, измененная таким образом, что пользователи могут включать ее, не изменяя проект, добавив ссылку на нее в каждый файл. Можно использовать код под лицензией Apache 2.0 с помощью GNU General Public License версии 3.0 (GPLv3);

• лицензии BSD 2-clause (лицензия из двух пунктов) и BSD 3-clause (лицензия из трех пунктов) () — последняя представляет собой лицензию из двух пунктов, которая также содержит дополнительное ограничение использования торговых марок издателя;

• лицензии Massachusetts Institute of Technology (MIT) () — версии Expat и X11 названы в честь популярных продуктов, использующих соответствующие лицензии;

• лицензия Internet Software Consortium (ISC) () практически идентична лицензии MIT, за исключением нескольких строк, сейчас она считается устаревшей.

• Свободные. Больше концентрируются на том, чтобы гарантировать, что исходный код, включая изменения, которые в него вносятся, будет доступен. Среди таких лицензий наиболее известно семейство GPL. Текущая версия лицензии этого семейства — GPLv3 ().

10932.png

Лицензия GPLv2 несовместима с Apache 2.0, поэтому код под лицензией GPLv2 нельзя объединять с кодом под лицензией Apache 2.0. Но проекты под этой лицензией могут быть использованы в проектах под лицензией GPLv3 (которые впоследствии тоже перейдут под лицензию GPLv3).

Лицензии, соответствующие критериям OSI, позволяют использовать код в коммерческих целях, модифицировать ПО и распространять его с разными ограничениями и требованиями. Все лицензии, перечисленные в табл. 4.5, ограничивают ответственность пользователя и требуют от него помнить об авторских правах и лицензии при любом распространении.

Таблица 4.5. Темы, рассматриваемые в популярных лицензиях

Семейство  лицензий

Ограничения

Разрешения

Требования

BSD

Защитить торговую марку издателя (BSD 3-clause)

Дает гарантию (BSD 2-clause и BSD 3-clause)

MIT (X11 или Expat), ISC

Защитить торговую марку издателя (ISC и MIT/X11)

Разрешает сублицензирование под другой лицензией

Apache версии 2.0

Защитить торговую марку издателя

Разрешает сублицензирование, использование в патентах

Необходимо указывать изменения, вносимые в исходный код

GPL

Запрещает сублицензирование под другой лицензией

Дает гарантию и можно (только в GPLv3) использовать в патентах

Необходимо указывать изменения, вносимые в исходный код, и включать исходный код

Лицензирование ресурсов

Книга Вана Линдберга (Van Lindberg) Intellectual Property and Open Source (издательство O’Reilly) — отличный ресурс, посвященный юридическим вопросам в отношении ПО с открытым исходным кодом. Эта книга поможет вам изучить лицензии и юридические тонкости, связанные с интеллектуальной собственностью (торговые марки, патенты, авторские права), а также их влияние на программы с открытым исходным кодом. Если вас не особо волнуют юридические моменты и вы хотите что-то быстро выбрать, вам могут помочь следующие сайты:

GitHub предоставляет удобное руководство (/), где сравниваются все лицензии в рамках нескольких предложений;

• на ресурсе TLDRLegal (/) перечислено, что можно и чего нельзя делать под каждой лицензией;

• список лицензий, одобренных OSI (), содержит полный текст всех лицензий, прошедших проверку на соответствие Open Source Definition (что позволит свободно использовать, модифицировать и распространять ПО).

Цитата изначально приведена Ральфом Уолдо Эмерсоном (Ralph Waldo Emerson) в эссе Self-Reliance. Присутствует в PEP 8 для того, чтобы подтвердить, что здравый смысл важнее руководства по стилю. Например, гармоничный код и существующие соглашения перевесят строгое следование PEP 8.

Разность — это утилита оболочки, которая сравнивает два файла и показывает отличающиеся строки.

В соответствии с PEP 8 это значение равно 80 символам. Согласно другим источникам — 100, а в вашем случае это значение зависит от того, что говорит ваш начальник. Ха! Честно говоря, любой, кто использовал консоль для отладки кода в полевых условиях, быстро оценит ограничение в 80 символов (при котором строка в консоли не переносится) и на деле будет использовать 75–77, чтобы можно было увидеть нумерацию строк в Vi.

Обратитесь к 14-му пункту «Дзена Питона». Гвидо, наш BDFL, — голландец.

Кстати, именно поэтому только хэшируемые объекты можно хранить во множествах или использовать как ключи для словарей. Чтобы ваши объекты Python стали хэшируемыми, определите функцию-член object.__hash__(self), которая возвращает целое число.

В этом случае метод __exit__() вызовет метод обертки для ввода/вывода close(), чтобы закрыть дескриптор файла. Во многих системах имеется максимальное количество открытых дескрипторов файлов, и хороший тон — освобождать их по завершении работы.

Если хотите, можете назвать файл my_spam.py, но даже нашим другом — нижним подчеркиванием — не следует злоупотреблять в именах модулей (нижнее подчеркивание наводит на мысль, что перед вами имя переменной).

Благодаря PEP 420 (/), который был реализован в Python 3.3, существует альтернативный корневой пакет — пакет пространства имен. Такие пакеты не должны содержать файл __init__.py, могут быть разбиты по нескольким каталогам sys.path. Python соберет все фрагменты воедино и представит их пользователю как один пакет.

Инструкции по созданию собственных типов с помощью C предоставлены в документации Python по адресу .

Пример простого алгоритма хэширования — преобразование байтов объекта в целое число и взятие его суммы по какому-нибудь модулю. memcached (/) распределяет ключи между несколькими компьютерами именно так.

Должны признать: хотя в PEP 3101 (/) форматирование с использованием знака % (%s, %d, %f) считается устаревшим, большинство разработчиков старой школы все еще используют его. В PEP 460 (/) был представлен такой же метод форматирования байтов или объектов bytearray.

Обратите внимание, что метод unittest.TestCase.tearDown не будет запущен, если в коде есть ошибки. Это может удивить вас, если вы использовали функциональность unit­test.mock для того, чтобы изменить реальное поведение кода. В Python 3.1 был добавлен метод unittest.TestCase.addCleanup(). Он помещает функцию очистки и ее аргументы в стек, и эта функция будет вызвана либо после метода unittest.Test­Case.tearDown(), либо в любом случае независимо от того, был ли вызван метод tearDown(). Для получения более подробной информации обратитесь к документации метода unittest.Test­Ca­se.add­Cleanup() (см. ).

Для тех, кому интересно: развернулась дискуссия о введении поддержки () в файлах README в PyPI.

Вы можете встретить и другие инструменты вроде Pycco, Ronn, Epydoc (больше не поддерживается) и MkDocs. Практически все используют Sphinx, мы также рекомендуем выбрать его.

На момент написания книги они находились под лицензией Academic Free License v.2.1 или Apache License, Version 2.0. Полное описание того, как это работает, вы можете найти на странице /.

Все лицензии, описанные здесь, одобрены OSI, вы можете узнать о них на главной странице OSI по адресу .

Аббревиатура tl;dr означает «Too long; didn’t read» («Слишком длинно, не читал») и, скорее всего, существовала как сокращение для редакторов, пока не стала популярной в Интернете.

Назад: Часть II. Переходим к делу
Дальше: 5. Читаем отличный код