Автор статьи: Янник Волф (Yannick Wolff). Оригинал: https://blog.sicara.com/perfect-python-command-line-interfaces-7d5d4efad6a2
Эта статья покажет вам как создавать отличные интерфейсы командной строки на Python, как повысить производительность вашей команды и сделать работу в ней более комфортной.
Как и все разработчики, пишущие на Python, мы также постоянно используем и пишем интерфейсы командной строки. Во время работы над моими научными проектами я, например, запускаю сразу несколько программ из командной строки, чтобы обучить мои модели и вычислить точность алгоритмов.
Вот почему, лучший способ повысить вашу производительность, это сделать ваши программы, по возможности, удобными и простыми. Особенно это важно, когда над общим проектом работают несколько разработчиков в одной команде.
Для того, чтобы этого достичь, я советую вам соблюдать эти 4 принципа:
Вы должны задать значения по умолчанию для всех аргументов, где это возможно.
Во всех случаях, когда могут возникнуть ошибки, необходимо их обрабатывать (например: не указан аргумент, неверный тип или не найден файл).
Все аргументы и параметры нужно обязательно задокументировать.
Для длительных заданий в командной строке должно показываться их текущее состояние.
Давайте возьмем простой пример
И попробуем применить эти правила к конкретному примеру: скрипту, который шифрует и расшифровывает сообщения, используя шифр Цезаря для этого.
Представьте, что у нас уже есть готовая функция для шифрования encrypt (ее код приведен ниже), и мы хотим написать простой скрипт, который позволит нам зашифровать и расшифровать какое-то сообщение.
При этом, мы хотим предоставить пользователю выбор режима между шифровкой (по умолчанию) и расшифровкой, задавать ключ (по умолчанию это 1), в качестве аргументов командной строки.
Но сначала наш скрипт должен уметь извлекать значения параметров командной строки. Когда я погуглил: «параметры в командной строке на python», первый результат, который я получил, был про sys.argv. Что ж, давайте попробуем этот метод…
Метод для «начинающих»
sys.argv — это список, в который будут помещены все параметры, вводимые пользователем, когда он запустит ваш скрипт (а также, в нем содержится и само название скрипта).
Например, если я напечатаю:
> python caesar_script.py --key 23 --decrypt my secret message
pb vhfuhw phvvdjh
Список будет содержать:
['caesar_script.py', '--key', '23', '--decrypt', 'my', 'secret', 'message']
Итак, теперь мы можем пробежаться в цикле по этому списку, найти в нем ключ '--key' (или '-k'), чтобы получить его значение и '--decrypt', чтобы включить режим расшифровки (на самом деле, мы просто будем использовать его противоположное значение в качестве ключа).
И окончательно, наш скрипт будет выглядеть как в этом фрагменте кода:
Этот скрипт более или менее отвечает рекомендациям, обозначенным выше:
Значение ключа и режим определены по умолчанию.
Основные случаи возникновения ошибок обработаны (например: если не введен исходный текст или не указаны параметры).
В этих случаях будет напечатана краткая справка, и в случае когда скрипт будет вызван без аргумента:
> python caesar_script_using_sys_argv.py
Usage: python caesar.py [ --key <key> ] [ --encrypt|decrypt ] <text>
Однако эта версия скрипта для шифра Цезаря довольно длинная (39 строк, и он даже не включает логику самого шифрования) и некрасива.
Но кажется, что для разбора аргументов командной строки должен существовать способ получше...
А как насчет argparse?
argparse — это стандартный модуль библиотеки Python для анализа аргументов командной строки.
Давайте посмотрим, как будет выглядеть наш скрипт, используя argparse для этого:
Он также соответствуют нашим рекомендациям и предоставляет более точную справку, делает более интерактивной обработку ошибок, в отличии от ранее рассмотренного нами самодельного скрипта:
> python caesar_script_using_argparse.py --encode My message
usage: caesar_script_using_argparse.py [-h] [-e | -d] [-k KEY] [text [text ...]]
caesar_script_using_argparse.py: error: unrecognized arguments: --encode
> python caesar_script_using_argparse.py --help
usage: caesar_script_using_argparse.py [-h] [-e | -d] [-k KEY] [text [text ...]]
positional arguments:
text
optional arguments:
-h, --help show this help message and exit
-e, --encrypt
-d, --decrypt
-k KEY, --key KEY
Однако, глядя на код, мне кажется, что его начало внутри моей функции (строки с 7 по 13), где определены аргументы, выглядит не очень элегантно: в нем слишком много строк и его сложно читать, тогда как, это можно легко исправить: сделать код компактнее и описать его в декларативном стиле.
И сделать это можно с помощью одного Clickа!
Нам повезло: у есть библиотека на для Python, которая предлагает те же функции, что и argparse (и даже чуть больше), но с более изящным стилем кода. Она называется Click.
Вот как будет выглядеть третья версия нашего скрипта, при использовании Click:
Обратите внимание, что теперь аргументы и параметры объявлены в декораторах, которые делают их доступными напрямую, в качестве параметров моей функции.
Позвольте, пояснить некоторые тонкости в приведенном выше коде:
Параметр nargs для аргумента, определяет количество слов, которые будут вводится в него, последовательно (в «строке в кавычках, как эта», считающей 1 словом). Его значение по умолчанию равно 1. А nargs = -1 будет означать, что можно указать любое количество слов.
Запись ключа --encrypt или --decrypt позволяет определить взаимоисключающие параметры (например, как в функции add_mutually_exclusive_group из argparse), которая приводит к логическому параметру.
Click.echo — это небольшая утилита, предоставляемая библиотекой, которая делает то же самое, что и print, но при этом совместима с версиями 2 и 3 Python, и имеет некоторые дополнительные функции (раскраска в цвета и т. п.).
Добавим немного таинственности
Предполагается, что аргументы в нашем скрипте, это суперсекретные сообщения, которые также нужно зашифровать. Разве не забавно, просить пользователя набирать текстовые сообщения прямо в своем терминале, оставляя их в истории команд?
Есть решение сделать это более безопасным способом: использовать скрытые символы в приглашении. Или, можно было бы, прочитать текст из входного файла — это гораздо практичнее для длинных текстов. Или, почему бы не предоставить выбор пользователю?
И, давайте сделаем то же самое для вывода готового текста: пользователь может сохранить его в файл или распечатать в терминале. Так мы приближаемся к нашей последней улучшенной версии скрипта для шифра Цезаря:
Что нового в этой версии?
Во-первых, обратите внимание, что я добавил параметр справки для каждого аргумента или параметра. Поскольку скрипт становится немного сложнее, он позволяет добавить в справку некоторые подробности о его работе, которая теперь выглядит следующим образом:
> python caesar_script_v2.py --help
Usage: caesar_script_v2.py [OPTIONS]
Options:
--input_file FILENAME File in which there is the text you want to encrypt/decrypt. If not provided, a prompt will allow you to type the input text.
--output_file FILENAME File in which the encrypted/decrypted text will be written. If not provided, the output text will just be printed.
-d, --decrypt / -e, --encrypt Whether you want to encrypt the input text or decrypt it.
-k, --key INTEGER The numeric key to use for the caesar encryption / decryption.
--help Show this message and exit.
Во вторых, у нас есть два новых параметра: input_file и output_file, имеющие тип click.File. Библиотека может открывать файлы в правильном режиме до передачи их в функцию и обрабатывать возможные ошибки. Например:
> python caesar_script_v2.py --decrypt --input_file wrong_file.txt
Usage: caesar_script_v2.py [OPTIONS]
Error: Invalid value for "--input_file": Could not open file: wrong_file.txt: No such file or directory
И в третьих, как объясняется в тексте справки, если input_file отсутствует, то мы используем click.prompt, чтобы позволить пользователю вводить свой текст прямо в поле, символы которого будут скрыты в режиме шифрования. Выглядит это теперь так:
> python caesar_script_v2.py --encrypt --key 2
Enter a text: **************
yyy.ukectc.eqo
Давайте взломаем шифр!
Допустим, вы хакер, и хотите расшифровать секретный текст, зашифрованный с помощью шифра Цезаря, но не знаете ключ.
Самая простая стратегия взлома может состоять в том, чтобы вызвать нашу функцию дешифрования 25 раз со всеми возможными ключами и прочитать все полученные тексты в поисках того, который имеет смысл.
Но так как вы умны и ленивы, то предпочли бы автоматизировать процесс. Одним из способов выбора наиболее вероятного оригинального текста из этих 25 текстов является подсчет количества реальных слов английского языка в них. Давайте сделаем это с помощью модуля PyEnchant:
Все работает четко, как часовой механизм, но если вы еще помните, есть одно правило для хорошего интерфейса в командной строке, которое здесь не соблюдается, а именно: «4. Для длительных заданий в командной строке должно показываться их текущее состояние».
На текст, который я использовал для проверки, состоящего из 10⁴ слов, скрипту требуется около 5 секунд для того, чтобы напечатать расшифрованный текст. Это вполне нормально, учитывая, что он должен проверить 25 значений ключа, для 10⁴ слов, проверяя при этом, принадлежат ли они английскому словарю.
Представьте, что вы хотите расшифровать текст, содержащий 10⁵ слов, тогда на печать каких-либо результатов потребуется 50 секунд, что может очень огорчить пользователя.
Вот почему, я рекомендую выводить строку состояния для таких задач. Ведь, как правило, это очень легко реализовать.
Тот же скрипт, печатающий индикатор выполнения задачи:
Видите какие-нибудь отличия? Их не так-то легко заметить, потому они состоят всего из 4 букв: TQDM.
Так называется библиотека для Python, а также это название ее уникального класса, с помощью которого, вы можете обернуть любой цикл, выводящий соответствующий ряд чисел:
for key in tqdm(range(26)):
И в результате мы получим прекрасную строку состояния. Мне даже не верится, что она может быть настолько хороша.
Кстати, Click тоже предоставляет аналогичную утилиту для печати индикаторов выполнения заданий (click.progress_bar), но я считаю, что она не настолько информативна, а код, при ее использовании, содержит больше символов.
И в заключении, надеюсь, что мне удалось убедить вас в том, что нужно приложить еще усилий, чтобы приобрести больше опыта и улучшить ваши программы.