Книга: Как устроен Python. Гид для разработчиков, программистов и интересующихся
Назад: 25. Библиотеки: пакеты и модули
Дальше: 27. В начале пути

26. Полноценный пример

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

26.1. cat.py

Ниже приведена реализация команды Unix cat на языке Python. Включен параметр для добавления нумерации строк (--number), но другие параметры cat не поддерживаются. Сохраните код в файле с именем cat.py:

#!/usr/bin/env python3

 

r"""Простая реализация команды unix ``cat``.

Поддерживается только параметр ``--number``.

Пример демонстрирует структуру файла

и полезные приемы программирования на Python.

 

Строка документации в тройных кавычках для всего модуля

(этот файл). Если импортировать этот модуль

и выполнить команду ``help(cat)``, вы сможете

убедиться в этом.

 

Строка документации также содержит ``документирующий тест``,

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

Модуль ``doctest`` может выполнить эту строку документации

и проверить ее правильность по выходным данным.

 

>>> import io

>>> fin = io.StringIO(\

 

... 'hello\nworld\n')

>>> fout = io.StringIO()

>>> cat = Catter([fin],

... show_numbers=True)

>>> cat.run(fout)

>>> print(fout.getvalue())

1 hello

2 world

 

"""

import argparse

import logging

import sys

 

__version__ = '0.0.1'

 

logging.basicConfig(

level=logging.DEBUG)

 

class Catter(object):

"""

Класс для конкатенации файлов

в стандартном выводе

 

Строка документации класса,

выводится командой ``help(cat.Catter)``

"""

 

def __init__(self, files,

show_numbers=False):

self.files = files

self.show_numbers = show_numbers

 

def run(self, fout):

# Использовать 6 пробелов для чисел

# с выравниванием по правому краю

fmt = '{0:>6} {1}'

for fin in self.files:

logging.debug('catting {0}'.format(fin))

for count, line in enumerate(fin, 1):

if self.show_numbers:

fout.write(fmt.format(

count, line))

else:

fout.write(line)

 

def main(args):

"""

Логика выполнения cat с аргументами

"""

parser = argparse.ArgumentParser(

description='Concatenate FILE(s), or '

'standard input, to standard output')

parser.add_argument('--version',

action='version', version=__version__)

parser.add_argument('-n', '--number',

action='store_true',

help='number all output lines')

parser.add_argument('files', nargs='*',

type=argparse.FileType('r'),

default=[sys.stdin], metavar='FILE')

parser.add_argument('--run-tests',

action='store_true',

help='run module tests')

args = parser.parse_args(args)

 

if args.run_tests:

import doctest

doctest.testmod()

else:

cat = Catter(args.files, args.number)

cat.run(sys.stdout)

logging.debug('done catting')

 

if __name__ == '__main__':

main(sys.argv[1:])

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

26.2. Что делает этот код?

Этот код осуществляет эхо-вывод содержимого файла (возможно, с нумерацией строк) на терминал в системах Windows и UNIX:

$ python3 cat.py -n README.md

1 # IllustratedPy3

2

3 If you have questions or concerns, click on Issues above.

Если вы запустите этот код с ключом -h, он выведет всю справочную документацию по аргументам командной строки:

$ python3 cat.py -h

usage: cat.py [-h] [--version] [-n] [--run-tests] [FILE [FILE ...]]

 

Concatenate FILE(s), or standard input, to standard output

 

positional arguments:

FILE

 

optional arguments:

-h, --help show this help message and exit

--version show program's version number and exit

-n, --number number all output lines

--run-tests run module tests

Функциональность разбора командной строки реализована в функции main и обеспечивается модулем argparse из стандартной библиотеки. Модуль argparse берет на себя всю работу по разбору аргументов.

Если вы хотите узнать, что делает модуль argparse, обратитесь к документации в интернете или воспользуйтесь функцией help. Основная идея заключается в том, что вы создаете экземпляр класса ArgumentParser и вызываете .add_argument для каждого параметра командной строки. Вы указываете параметры командной строки, сообщаете, какое действие необходимо для них выполнить (по умолчанию это сохранение значения, следующего за параметром), и предоставляете справочную документацию. После добавления аргументов вызывается метод .parse_args для аргументов командной строки (которые берутся из sys.argv). Результат .parse_args представляет собой объект, к которому присоединены атрибуты, имена которых определяются именами параметров. В данном случае это будут атрибуты .files и .number.

ПРИМЕЧАНИЕ

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

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

>>> import argparse

>>> argparse.__file__

'/usr/local/Cellar/python3/3.6.0/Frameworks/

Python.framework/Versions/3.6/lib/python3.6/argparse.py'

Так как код написан на Python (часть модулей написана на C), вы можете просмотреть файл с исходным кодом. К настоящему моменту вы уже сможете прочитать модуль и понять, что он должен делать.

После разбора аргументов программа создает экземпляр Catter, определенный в коде, и вызывает для него метод .run.

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

26.3. Типичная структура

Основные компоненты модуля Python в порядке их следования:

• #!/usr/bin/env python3 (если модуль также используется в качестве сценария);

• строка документации модуля;

• imports;

• метаданные и глобальные переменные;

• операции с журналом;

• реализация;

• if __name__ == '__main__': (если модуль также используется в качестве сценария);

• argparse.

ПРИМЕЧАНИЕ

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

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

26.4. #!

Первая строка файла, используемого в качестве сценария, содержит последовательность символов #!/usr/bin/env python3. В операционных системах семейства Unix эта строка обрабатывается для определения того, как следует выполнять сценарий. Соответственно, эта строка включается только в те файлы, которые должны выполняться в качестве сценариев.

В ней должна использоваться команда python 3, так как команда python в большинстве систем относится к Python версии 2.

ПРИМЕЧАНИЕ

На платформе Windows строка #! игнорируется, поэтому ее включение безопасно. Вы найдете ее во многих библиотеках, популярных в системах Windows.

ПРИМЕЧАНИЕ

Вместо того чтобы жестко задавать конкретный путь к исполняемому файлу Python, /usr/bin/env выбирает первый исполняемый файл python3 из каталогов PATH пользователя. Такие средства, как venv, изменяют содержимое PATH для использования альтернативных исполняемых файлов python3; они успешно работают в этой схеме.

СОВЕТ

Если в системах UNIX каталог с файлом присутствует в переменной среды PATH текущего пользователя, а файл является исполняемым, то для выполнения из командного интерпретатора достаточно указать только имя файла.

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

$ chmod +x <путь/к/file.py>

 

26.5. Строка документации

В начале файла модуля может располагаться строка документации уровня модуля. Она должна следовать за строкой с #!, но предшествовать любому коду Python. Строка документации содержит обзор модуля, и в ней должна содержаться сводная информация о коде. Кроме того, в ней могут содержаться примеры использования модуля.

СОВЕТ

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

В файле cat.py код doctest содержится в конце строки документации. При запуске cat.py с ключом --run-tests библиотека doctest перебирает все существующие строки документации и проверяет содержащийся в них код. Данная возможность приведена только для демонстрации: обычно возможность выполнения тестов в сценарии не отображается для рядовых пользователей, не являющихся разработчиками, даже если вы включили код doctest в строку документации. В данном случае параметр --run-test включен как пример использования модуля doctest.

26.6. Импортирование

Команды импортирования обычно включаются в начало модулей Python. Строки import принято группировать по местонахождению библиотеки. Сначала перечисляются библиотеки, входящие в стандартную библиотеку Python. Далее следуют сторонние библиотеки, и в последнюю очередь перечисляются библиотеки, локальные для текущего кода. Такая структура кода позволит конечным пользователям быстро просмотреть команды импорта, требования и источники происхождения кода.

26.7. Метаданные и глобальные переменные

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

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

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

>>> GOLDEN_RATIO = 1.618

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

ПРИМЕЧАНИЕ

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

Другая проблема с «волшебными числами» заключается в том, что значения со временем нередко распространяются в коде. Это не создаст проблем, пока вы не захотите изменить это значение. Что делать — провести поиск с заменой? А если «волшебное число» на самом деле представляет два разных значения — например, число сторон треугольника и размерность окружающего мира? В этом случае глобальный поиск с заменой приведет к появлению ошибок.

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

Кроме глобальных переменных, на этом уровне также существуют переменные метаданных. В метаданных хранится информация о модуле: автор, версия и т.д. Обычно метаданные хранятся в «специальных» переменных с двойными подчеркиваниями (__author__). Например, PEP 396 рекомендует хранить версию модуля в строковой переменной __version__ на глобальном уровне модуля.

ПРИМЕЧАНИЕ

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

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

__author__ = 'Matt Harrison'

__date__ = 'Jan 1, 2017'

__contact__ = 'matt_harrison <at> someplace.com'

__version__ = '0.1.1'

26.8. Операции с журналом

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

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

26.9. Другие глобальные переменные

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

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

26.10. Реализация

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

26.11. Тестирование

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

Другая интересная возможность doctest — проверка документации. Если ваши фрагменты когда-то работали, а теперь не работают, значит, либо изменился ваш код, либо сами фрагменты содержат ошибки. Это можно легко выявить до того, как пользователи начнут жаловаться вам.

СОВЕТ

Код doctest может размещаться в отдельном текстовом файле. Чтобы выполнить произвольный файл средствами doctest, используйте функцию testfile:

import doctest

doctest.testfile('module_docs.txt')

 

ПРИМЕЧАНИЕ

Кроме doctest стандартная библиотека Python включает модуль unittest, реализующий типичную методологию xUnit — подготовка/проверка/завершение. У обоих стилей тестирования — doctest и unittest — есть как достоинства, так и недостатки. Стиль doctest обычно создает больше трудностей с отладкой, а стиль unittest содержит шаблонный код, который считается слишком «завязанным на Java». Вы можете сочетать оба стиля, чтобы получить хорошо документированный и хорошо протестированный код.

26.12. if __name__ == '__main__':

Если ваш файл должен выполняться в качестве сценария, в конце сценария вы найдете следующий фрагмент:

if __name__ == '__main__':

sys.exit(main(sys.argv[1:]) or 0)

Чтобы понять эту команду, необходимо понимать смысл переменной __name__.

26.13. __name__

Python определяет переменную уровня модуля __name__ для любого импортируемого модуля или любого выполняемого файла. Обычно значением __name__ является имя модуля:

>>> import sys

>>> sys.__name__

'sys'

>>> import xml.sax

>>> xml.sax.__name__

'xml.sax'

У этого правила есть исключение. При выполнении модуля (то есть python3 some_module.py) значением __name__ является строка "__main__".

По сути, значение __name__ сообщает, загружается ли файл как библиотека или же выполняется как сценарий.

ПРИМЕЧАНИЕ

Использование __name__ можно продемонстрировать на простом примере. Создайте файл some_module.py со следующим кодом:

print("The __name__ is: {0}".format(__name__))

Теперь запустите REPL и импортируйте этот модуль:

>>> import some_module

The __name__ is: some_module

А теперь выполните этот модуль:

$ python3 some_module.py

The __name__ is: __main__

 

Одна из распространенных идиом в мире Python — размещение подобных проверок в конце модуля, который также может служить сценарием. Такая проверка определяет, выполняется файл или импортируется:

if __name__ == '__main__':

# выполнение

sys.exit(main(sys.argv[1:]) or 0)

Эта простая команда запускает функцию main, когда файл выполняется. И наоборот, если файл используется в качестве модуля, функция main автоматически выполняться не будет. Функция sys.exit вызывается с возвращаемым значением main (или 0, если main не возвращает значение), как делают все добропорядочные программы в мире Unix.

Функция main получает параметры командной строки в списке sys.argv. В самом начале sys.argv находится элемент python3, поэтому нам приходится создать срез sys.argv, чтобы исключить этот элемент перед тем, как передавать параметры main.

СОВЕТ

Некоторые разработчики размещают логику выполнения (код, расположенный внутри функции main) прямо под проверкой if __name__ == '__main__':. Несколько причин для хранения логики в функции:

• Функция main может вызываться из других мест.

• Функцию main можно легко тестировать с разными аргументами.

• Сокращение объема кода, выполняемого на глобальном уровне.

 

26.14. Итоги

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

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

26.15. Упражнения

1. Скопируйте код cat.py. Добейтесь того, чтобы он заработал на вашем компьютере. Не думайте, что это напрасный труд — занимаясь программированием, вы очень часто не создаете что-то с нуля, а повторно используете код, написанный другими людьми.

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

• Имя входного файла.

• Входная кодировка (по умолчанию UTF-8).

• Выходная кодировка.

• Режим обработки ошибок (игнорировать/выдать исключение).

/

Назад: 25. Библиотеки: пакеты и модули
Дальше: 27. В начале пути