Как писать отличные интерфейсы командной строки на Python. Изучаем на примере

argparse, click

Card image cap

Автор статьи: Янник Волф (Yannick Wolff). Оригинал: https://blog.sicara.com/perfect-python-command-line-interfaces-7d5d4efad6a2


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


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

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

Для того, чтобы этого достичь, я советую вам соблюдать эти 4 принципа:

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

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

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

  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', чтобы включить режим расшифровки (на самом деле, мы просто будем использовать его противоположное значение в качестве ключа).

И окончательно, наш скрипт будет выглядеть как в этом фрагменте кода:


Этот скрипт более или менее отвечает рекомендациям, обозначенным выше:

  1. Значение ключа и режим определены по умолчанию.

  2. Основные случаи возникновения ошибок обработаны (например: если не введен исходный текст или не указаны параметры).

  3. В этих случаях будет напечатана краткая справка, и в случае когда скрипт будет вызван без аргумента:

> 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:


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

Позвольте, пояснить некоторые тонкости в приведенном выше коде:

  1. Параметр nargs для аргумента, определяет количество слов, которые будут вводится в него, последовательно (в «строке в ​​кавычках, как эта», считающей 1 словом). Его значение по умолчанию равно 1. А nargs = -1 будет означать, что можно указать любое количество слов.

  2. Запись ключа --encrypt или --decrypt позволяет определить взаимоисключающие параметры (например, как в функции add_mutually_exclusive_group из argparse), которая приводит к логическому параметру.

  3. 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), но я считаю, что она не настолько информативна, а код, при ее использовании, содержит больше символов.

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

1 447 просмотров

Комментарии