Модуль click в Python, создание CLI интерфейсов
Модуль click — это пакет Python для создания красивых интерфейсов командной строки компонуемым способом с минимальным количеством кода, насколько это необходимо. Он легко настраивается, но по умолчанию поставляется с разумными настройками.
Он направлен на то, чтобы сделать процесс написания инструментов командной строки быстрым и увлекательным, а также предотвратить любое разочарование, вызванное невозможностью реализовать предполагаемый API CLI.
- Произвольное вложение команд.
- Автоматическое создание справки по параметрам командной строки.
- Поддерживает отложенную загрузку подкоманд во время выполнения.
- Меньшее количество кода по сравнению с argparse .
Для быстрого создания утилит командной строки из имеющихся функций можно использовать сторонний модуль fire .
Почему именно click , а не встроенный модуль argparse ?
Модуль argparse имеет некоторые особенности поведения, которые затрудняют обработку произвольных интерфейсов командной строки:
- argparse имеет встроенное поведение, которое пытается угадать, является ли что-то параметром или опцией. Такое поведение становится непредсказуемым при работе со сценариями, в которых не используется часть опций и/или параметров.
- argparse не поддерживает отключение перемежающихся аргументов. Без этой функции невозможно безопасно реализовать вложенный синтаксический анализ, например как в click .
Установка модуля click в виртуальное окружение.
# создаем виртуальное окружение, если нет $ python3 -m venv .venv --prompt VirtualEnv # активируем виртуальное окружение $ source .venv/bin/activate # ставим модуль click (VirtualEnv):~$ python3 -m pip install -U click
Содержание:
- Простой пример сценария с модулем click ;
- Модуль click и пакет setuptools ;
- Создание команды;
- Функция click.echo() ;
- Вложенные команды;
- Отложенная регистрация подкоманд;
- Добавление параметров командной строки.
Простой пример сценария с модулем click :
import click @click.command() @click.option('--count', default=1, help='Number of greetings.') @click.option('--name', prompt='Your name', help='The person to greet.') def hello(count, name): """Приветствует ИМЯ (`name`), несколько (`count`) раз.""" for x in range(count): click.echo(f"Hello name>!") if __name__ == '__main__': hello()
Если запустить эту программу в командной строке то вывод будет следующим:
$ python hello.py --count=3 # Your name: John # Hello John! # Hello John! # Hello John!
К тому же программа, на основе модуля click автоматически генерирует красивые справочные страницы:
$ python hello.py --help # Usage: hello.py [OPTIONS] # # Simple program that greets NAME for a total of COUNT times. # # Options: # --count INTEGER Number of greetings. # --name TEXT The person to greet. # --help Show this message and exit.
Модуль click и пакет setuptools .
В коде программы, есть блок в конце файла, который выглядит следующим образом: if __name__ == ‘__main__’ : . Традиционно так выглядит автономный файл Python, но есть способ cделать использование написанной утилиты командной строки лучше и проще с помощью инструментов | setuptools |.
Для этого есть две основные (и многие другие) причины:
Во-первых, setuptools автоматически генерирует исполняемые оболочки для Windows, следовательно утилиты командной строки работают и в Windows.
Вторая причина заключается в том, что сценарии setuptools работают с virtualenv в Unix без необходимости активации virtualenv . Это очень полезная концепция, которая позволяет объединить написанные скрипты со всеми зависимостями в виртуальную среду virtualenv .
Дополнительную информацию смотрите в разделе «Интеграция модуля click с setuptools «.
Базовые концепции модуля click .
Создание команды.
Модуль click основан на объявлении команд через декораторы. Внутри модуля есть интерфейс без декоратора для сложных случаев использования, но он не рекомендуется для высокоуровневого кода.
Функция становится инструментом командной строки, если она декорируется с помощью @click.command() . В самом простом случае, если просто украсить функцию этим декоратором, то она превратится в вызываемый скрипт:
# hello.py import click @click.command() def hello(): click.echo('Hello World!') if __name__ == '__main__': hello()
Происходит то, что декоратор @click.command() преобразует функцию в команду, которая затем может быть вызвана:
$ python hello.py # Hello World! # И соответствующая страница помощи $ python hello.py --help # Usage: hello.py [OPTIONS] # # Options: # --help Show this message and exit.
Функция click.echo() .
Почему в этом примере используется функция click.echo() вместо обычной функции print() ? Ответ на этот вопрос заключается в том, что Модуль click пытается последовательно поддерживать различные среды и быть очень надежным, даже если среда настроена неправильно. Click спроектирован, что-бы быть функциональным, по крайней мере, на базовом уровне, даже если все полностью сломано.
Это означает, что функция click.echo() применяет некоторое исправления ошибок в случае, если кодировка терминала настроена неправильно.
Функция click.echo() также поддерживает цвет и другие стили вывода. Она автоматически удалит стили, если выходной поток является файлом. В Windows автоматически устанавливается и используется модуль colorama .
Вложенные команды.
Для простых сценариев командной строки можно автоматически присоединить и создать подкоманду, при помощи декоратора @click.group() .
import click # создаем группу команд `cli` @click.group() def test(): pass # обратите внимание на название # декораторов для вложенных команд # присоединяем команду `initdb` @test.command() def initdb(): click.echo('Initialized the database') # присоединяем команду `dropdb` @test.command() def dropdb(): click.echo('Dropped the database') if __name__ == '__main__': test()
В примере выше, декоратор @click.group() расположен над основной функцией/командой test() и создает объект Group с именем этой функции test , которому можно дать несколько подкоманд. Подкоманды украшаются декораторами @group.command() , в примере, это декоратор с именем @test.command() .
Отложенная регистрация подкоманд.
Вместо использования декоратора @group.command() , подкоманды могут быть украшены простым декоратором @click.command() и позже зарегистрированы в группе при помощи group.add_command() . Такое поведение может быть использовано для разделения подкоманд на несколько модулей Python.
import click # создаем команду, с именем `initdb` @click.command() def initdb(): click.echo('Initialized the database') # создаем команду, с именем `dropdb` @click.command() def dropdb(): click.echo('Dropped the database') # создаем группу команд, с именем `cli` @click.group() def test(): pass # добавляем `initdb` и `dropdb` как подкоманды сценария `test` test.add_command(initdb) test.add_command(dropdb) if __name__ == '__main__': test()
Добавление параметров командной строки.
Чтобы добавить параметры командной строки к сценарию, необходимо использовать декораторы @click.option() и @click.argument() :
# hello.py import click @click.command() @click.option('--count', default=1, help='number of greetings') @click.argument('name', help='You name') def hello(count, name): """ This script prints "Hello !" COUNT times. - is your name. """ for x in range(count): click.echo(f"Hello name>!") if __name__ == '__main__': hello()
$ python hello.py --count=3 John # Hello John! # Hello John! # Hello John!
Теперь с опцией —help :
$ python hello.py --help # Usage: hello.py [OPTIONS] NAME # # This script prints "Hello !" COUNT times. # # - is your name. # # Options: # --count INTEGER number of greetings # --help Show this message and exit.
- КРАТКИЙ ОБЗОР МАТЕРИАЛА.
- Интеграция модуля click с setuptools
- Cтиль и цвета при выводе текста в терминал, модуль click
- Опции сценария командной строки модуля click
- Позиционные параметры командной строки модуля click
- Встроенные типы опций и параметров модуля click
- Произвольное вложение команд в сценариях модуля click
- Запрос на ввод данных, подтверждение действий в сценариях модуля click
- Настройка страницы справки сценария на click
- Индикатор выполнения для модуля click
- Прокрутка длинного текста в терминале с модулем click
- Ожидание нажатия клавиши в сценарии click
- Запуск приложений ОС из сценария на click
Обработка аргументов командной строки. Библиотеки sys и argparse. Декораторы
Обработка аргументов командной строки. Запуск программы с аргументами
Параметры запуска, задаваемые через командную строку, чаще всего используют консольные программы, хотя программы с графическим интерфейсом тоже не брезгуют этой возможностью. Наверняка в жизни каждого программиста была ситуация, когда приходилось разбирать параметры командной строки, как правило, это не самая интересная часть программы, но без нее не обойтись. Эта статья посвящена тому, как Python облегчает жизнь программистам при решении этой задачи благодаря своей стандартной библиотеке argparse.
Примеры без использования argparse
Путь для начала у нас есть простейший скрипт на Python. Для определенности назовем скрипт coolprogram.py, это будет классический Hello World, над которым мы будем работать
if __name__ == "__main__": print ("Привет, мир!")
Мы завершили эту сложнейшую программу и отдали ее заказчику, он доволен, но просит добавить в нее возможность указывать имя того, кого приветствуем, причем этот параметр может быть не обязательным. Т.е. программа может использоваться двумя путями:
$ python coolprogram.py
$ python coolprogram.py Вася
Мы можем воспользоваться переменной argv из модуля sys. sys.argv содержит список параметров, переданных программе через командную строку, причем нулевой элемент списка — это имя нашего скрипта. Т.е. если у нас есть следующий скрипт с именем params.py:
import sys if __name__ == "__main__": for param in sys.argv: print (param)
и мы запускаем его с помощью команды
python params.py
то в консоль будет выведена единственная строка:
params.py
Если же мы добавим несколько параметров,
python params.py param1 param2 param3
то эти параметры мы увидим в списке sys.argv, начиная с первого элемента:
params.py param1 param2 param3
Здесь можно обратить внимание на то, что ссылка на интерпретатор Python в список этих параметров не входит, хотя он также присутствует в строке вызова нашего скрипта.
Вернемся к нашей задаче. Погрузившись в код на неделю, мы могли бы выдать заказчику следующий скрипт:
import sys if __name__ == "__main__": if len (sys.argv) > 1: print ("Привет, <>!".format (sys.argv[1] ) ) else: print ("Привет, мир!")
Теперь, если программа вызывается с помощью команды
python coolprogram.py
то результат будет прежний
Привет, мир!
а если мы добавим параметр:
python coolprogram.py Вася
то программа поприветствует некоего Васю:
Привет, Вася!
Пока все легко и никаких проблем не возникает. Теперь предположим, что требования заказчика вновь изменились, и на этот раз он хочет, чтобы имя приветствуемого человека передавалось после именованного параметра —name или -n, причем нужно следить, что в командной строке передано только одно имя. С этого момента у нас начнется вермишель из конструкций if.
import sys if __name__ == "__main__": if len (sys.argv) == 1: print ("Привет, мир!") else: if len (sys.argv) 3: print ("Ошибка. Слишком мало параметров.") sys.exit (1) if len (sys.argv) > 3: print ("Ошибка. Слишком много параметров.") sys.exit (1) param_name = sys.argv[1] param_value = sys.argv[2] if (param_name == "--name" or param_name == "-n"): print ("Привет, <>!".format (param_value) ) else: print ("Ошибка. Неизвестный параметр '<>'".format (param_name) ) sys.exit (1)
Здесь мы проверяем ситуацию, что мы вообще не передали ни одного параметра, потом проверяем, что дополнительных параметров у нас ровно два, что они называются именно —name или -n, и, если нас все устраивает, выводим приветствие.
Как видите, код превратился в тихий ужас. Изменить логику работы в нем в дальнейшем будет очень сложно, а при увеличении количества параметров нужно будет срочно применять объектно-ориентированные меры по отделению логики работы программы от разбора командной строки. Разбор командной строки мы могли бы выделить в отдельный класс (или классы), но мы этого здесь делать не будем, поскольку все уже сделано в стандартной библиотеке Python, которая называется argparse.
Но перед тем, как перейти к библиотеке argparse, еще немного остановимся на sys. Модуль sys обеспечивает доступ к некоторым переменным и функциям, взаимодействующим с интерпретатором python. Самыми полезными являются:
- sys.argv — список аргументов командной строки, передаваемых сценарию Python. sys.argv[0] является именем скрипта (пустой строкой в интерактивной оболочке).
- sys.exit([arg]) — выход из Python. Функция exit принимает необязательный аргумент, обычно целое число, которое дает статус выхода. Ноль считается как успешное завершение. Обязательно проверьте, имеет ли ваша операционная система какие-либо особые значения для своих статусов выхода, чтобы вы могли следить за ними в своем собственном приложении. Обратите внимание на то, что когда вы вызываете exit, это вызовет исключение SystemExit, которое позволяет функциям очистки работать в конечных пунктах блоков try / except.
- sys.stdin — стандартный поток ввода.
- sys.stdout — стандартный поток вывода.
- sys.stderr — стандартный поток ошибок. Stdin, stdout и stderr сопоставляются с файловыми объектами, которые соответствуют стандартным входам, выходам и потокам ошибок интерпретатора соответственно. Функция stdin используется для всех входов, используемых интерпретатором (за исключением скриптов), тогда как stdout используется для выходов операторов print. Эти потоки вывода можно переопределить, например для перенаправления логов вывода в графический интерфейс или в файл.
- sys.__stdin__, sys.__stdout__, sys.__stderr__ — исходные значения потоков ввода, вывода и ошибок.
Использование библиотеки argparse
Простейший случай
Как как было сказано выше, стандартная библиотека argparse предназначена для облегчения разбора командной строки. На нее можно возложить проверку переданных параметров: их количество и обозначения, а уже после того, как эта проверка будет выполнена автоматически, использовать полученные параметры в логике своей программы.
Основа работы с командной строкой в библиотеке argparse является класс ArgumentParser. У его конструктора и методов довольно много параметров, все их рассматривать не будем, поэтому в дальнейшем рассмотрим работу этого класса на примерах, попутно обсуждая различные параметры.
Простейший принцип работы с argparse следующий:
- Создаем экземпляр класса ArgumentParser.
- Добавляем в него информацию об ожидаемых параметрах с помощью метода add_argument (по одному вызову на каждый параметр).
- Разбираем командную строку помощью метода parse_args, передавая ему полученные параметры командной строки (кроме нулевого элемента списка sys.argv).
- Начинаем использовать полученные параметры.
Для начала перепишем программу coolprogram.py с единственным параметром так, чтобы она использовала библиотеку argparse. Напомню, что данном случае мы ожидаем следующий синтаксис параметров:
python coolprogram.py [Имя]
Здесь [Имя] является необязательным параметром.
Наша программа с использованием argparse может выглядеть следующим образом:
import sys import argparse def createParser (): parser = argparse.ArgumentParser() parser.add_argument ('name', nargs='?') return parser if __name__ == '__main__': parser = createParser() namespace = parser.parse_args() print (namespace) if namespace.name: print ("Привет, <>!".format (namespace.name) ) else: print ("Привет, мир!")
На первый взгляд эта программа работает точно так же, как и раньше, хотя есть отличия, но мы их рассмотрим чуть позже. Пока разберемся с тем, что мы понаписали в программе.
Создание парсера вынесено в отдельную функцию, поскольку эта часть программы в будущем будет сильно изменяться и разрастаться. Сначала мы создали экземпляр класса ArgumentParser с параметрами по умолчанию. Что это за параметры, опять же, поговорим чуть позже.
Далее мы добавили ожидаемый параметр в командной строке с помощью метода add_argument. При этом такой параметр будет считаться позиционным, т.е. он должен стоять именно на этом месте и у него не будет никаких предварительных обозначений (мы их добавим позже в виде ‘-n’ или ‘—name’). Если бы мы не добавили именованный параметр nargs=’?’, то этот параметр был бы обязательным. nargs может принимать различные значения. Если бы мы ему присвоили целочисленное значение больше 0, то это бы означало, что мы ожидаем ровно такое же количество передаваемых параметров (точнее, считалось бы, что первый параметр ожидал бы список из N элементов, разделенных пробелами, этот случай мы рассмотрим позже). Также этот параметр может принимать значение ‘?’, ‘+’, ‘*’ и argparse.REMAINDER. Мы их не будем рассматривать, поскольку они важны в сочетании с необязательными именованными параметрами, которые могут располагаться как до, так и после нашего позиционного параметра. Тогда этот параметр будет показывать как интерпретировать список параметров, где будет заканчиваться один список параметров и начинаться другой.
Итак, мы создали парсер, после чего можно вызвать его метод parse_args для разбора командной строки. Если мы не укажем никакого параметра, это будет означать равносильно тому, что мы передадим в него все параметры из sys.argv кроме нулевого, который содержит имя нашей программы. т.е.
parser.parse_args (sys.argv[1:])
В качестве результата мы получим экземпляр класса Namespace, который будет содержать в качестве члена имя нашего параметра. Теперь посмотрим, чему же равны наши параметры.
Если мы это сделаем и запустим программу с переданным параметром
python coolprogram.py Вася
, то увидим его в пространстве имен.
Namespace(name='Вася')
Если же теперь мы запустим программу без дополнительных параметров, то это значение будет равно None:
Namespace(name=None)
Мы можем изменить значение по умолчанию, что позволит нам несколько сократить программу. Пусть по умолчанию используется слово ‘мир’, ведь мы его приветствуем, если параметры не переданы. Для этого воспользуемся дополнительным именованным параметром default в методе add_argument.
import sys import argparse def createParser (): parser = argparse.ArgumentParser() parser.add_argument ('name', nargs='?', default='мир') return parser if __name__ == '__main__': parser = createParser() namespace = parser.parse_args (sys.argv[1:]) # print (namespace) print ("Привет, <>!".format (namespace.name) )
Программа продолжает работать точно также, как и раньше. Вы, наверное, заметили, что в предыдущем примере в метод parse_args ередаются параметры командной строки из sys.argv. Это сделано для того, чтобы показать, что список параметров мы можем передавать явно, при необходимости мы его можем предварительно обработать, хотя это вряд ли понадобится, ведь почти всю обработку можно возложить на плечи библиотеки argparse.
Добавляем именованные параметры
Теперь снова переделаем нашу программу таким образом, чтобы использовать именованные параметры. Напомню, что согласно последнему желанию (в смысле, для данной программы) заказчика имя приветствуемого человека должно передаваться после параметра —name или -n. С помощью pyparse сделать это проще простого — достаточно в качестве первых двух параметров метода add_argument передать эти имена параметров.
import sys import argparse def createParser (): parser = argparse.ArgumentParser() parser.add_argument ('-n', '--name', default='мир') return parser if __name__ == '__main__': parser = createParser() namespace = parser.parse_args(sys.argv[1:]) # print (namespace) print ("Привет, <>!".format (namespace.name) )
Теперь, если мы запустим программу без параметров, то увидим знакомое «Привет, мир!», а если мы запустим программу с помощью команды
python coolprogram.py -n Вася
python coolprogram.py --name Вася
То приветствовать программа будет Васю. Обратите внимание, что теперь в методе add_argument мы убрали параметр nargs=’?’ , поскольку все именованные параметры считаются необязательными. А если они не обязательные, то возникает вопрос, как поведет себя argparse, если этот параметр не передан? Для этого уберем параметр default в add_argument.
import sys import argparse def createParser (): parser = argparse.ArgumentParser() parser.add_argument ('-n', '--name') return parser if __name__ == '__main__': parser = createParser() namespace = parser.parse_args(sys.argv[1:]) print ("Привет, <>!".format (namespace.name) )
Если теперь запустить программу без параметров, то увидим приветствие великого None:
Привет, None!
Таким образом, если значение по умолчанию не указано, то оно считается равным None.
До этого мы задавали два имени для одного и того же параметра: длинное имя, начинающееся с «—» (—name) и короткое сокращение, начинающее ся с «-» (-n). При этом получение значение параметра из пространства имен осуществляется по длинному имени:
print ("Привет, <>!".format (namespace.name) )
Если мы не зададим длинное имя, то придется обращаться к параметру через его короткое имя (n):
import sys import argparse def createParser (): parser = argparse.ArgumentParser() parser.add_argument ('-n') return parser if __name__ == '__main__': parser = createParser() namespace = parser.parse_args(sys.argv[1:]) print (namespace) print ("Привет, <>!".format (namespace.n) )
При этом пространство имен будет выглядеть как:
Namespace(n='Вася')
Хорошо, с уменьшением количества имен параметров разобрались, но мы можем еще и увеличить количество имен, например, мы можем добавить для того же параметра еще новое имя —username, для этого достаточно его добавить следующим параметром метода add_argument:
import sys import argparse def createParser (): parser = argparse.ArgumentParser() parser.add_argument ('-n', '--name', '--username') return parser if __name__ == '__main__': parser = createParser() namespace = parser.parse_args(sys.argv[1:]) print (namespace) print ("Привет, <>!".format (namespace.name) )
Теперь мы можем использовать три варианта передачи параметров:
python coolprogram.py -n Вася python coolprogram.py —name Вася python coolprogram.py —username Вася
Все три варианта равнозначны, при этом надо обратить внимание, что при получении значения этого параметра используется первое длинное имя, т.е. name. Пространство имен при использовании всех трех вариантов вызова программы будет выглядеть одинаково:
Namespace(name='Вася')
Для полного погружения во все сложные случаи разбора параметров, можете ознакомиться со статьей https://jenyay.net/Programming/Argparse
Упражнение 1
Напишите консольную программу, которой на вход подается единственное число N (без имени или с именем -n), а программа печатает значение Nго числа Фибоначчи
Декораторы
Декораторы в Python и примеры их практического использования.
Итак, что же это такое? Для того, чтобы понять, как работают декораторы, в первую очередь следует вспомнить, что функции в python являются объектами, соответственно, их можно возвращать из другой функции или передавать в качестве аргумента. Также следует помнить, что функция в python может быть определена и внутри другой функции.
Вспомнив это, можно смело переходить к декораторам. Декораторы — это, по сути, «обёртки», которые дают нам возможность изменить поведение функции, не изменяя её код.
Создадим свой декоратор «вручную»:
def my_shiny_new_decorator(function_to_decorate): # Внутри себя декоратор определяет функцию-"обёртку". Она будет обёрнута вокруг декорируемой, # получая возможность исполнять произвольный код до и после неё. def the_wrapper_around_the_original_function(): print("Я - код, который отработает до вызова функции") function_to_decorate() # Сама функция print("А я - код, срабатывающий после") # Вернём эту функцию return the_wrapper_around_the_original_function # Представим теперь, что у нас есть функция, которую мы не планируем больше трогать. def stand_alone_function(): print("Я простая одинокая функция, ты ведь не посмеешь меня изменять?") stand_alone_function() # Однако, чтобы изменить её поведение, мы можем декорировать её, то есть просто передать декоратору, # который обернет исходную функцию в любой код, который нам потребуется, и вернёт новую, # готовую к использованию функцию: stand_alone_function_decorated = my_shiny_new_decorator(stand_alone_function) stand_alone_function_decorated()
Возможно мы бы хотели, чтобы каждый раз, во время вызова stand_alone_function, вместо неё вызывалась stand_alone_function_decorated. Для этого просто перезапишем stand_alone_function:
stand_alone_function = my_shiny_new_decorator(stand_alone_function) stand_alone_function()
Собственно, это и есть декораторы. Вот так можно было записать предыдущий пример, используя синтаксис декораторов:
@my_shiny_new_decorator def another_stand_alone_function(): print("Оставь меня в покое") another_stand_alone_function()
То есть, декораторы в python — это просто синтаксическая обертка для конструкций вида:
another_stand_alone_function = my_shiny_new_decorator(another_stand_alone_function)
Можно использовать несколько декораций для функций:
def bread(func): def wrapper(): print() func() print("<\______/>") return wrapper def ingredients(func): def wrapper(): print("#помидоры#") func() print("~салат~") return wrapper def sandwich(food="--ветчина--"): print(food) sandwich() sandwich = bread(ingredients(sandwich)) sandwich()
И аналогично через декораторы:
@bread @ingredients def sandwich(food="--ветчина--"): print(food) sandwich()
Не забываем, что так как порядок вызова функций имеет значение, то и порядок проставление декораторов так же имеет значение.
Упражнение 2
Напишите функцию, которая получает на вход список чисел и выдает ответ сколько в данном списке четных чисел. Напишите декоратор, который меняет поведение функции следующим образом: если четных чисел нет, то пишет «Нет(» а если их больше 10, то пишет «Очень много»
Передача декоратором аргументов в функцию
Однако, все декораторы, которые мы рассматривали, не имели одного очень важного функционала — передачи аргументов декорируемой функции. Собственно, это тоже несложно сделать.
Текстовый данные в языке пайтон описываются классом str:
def a_decorator_passing_arguments(function_to_decorate): def a_wrapper_accepting_arguments(arg1, arg2): print("Смотри, что я получил:", arg1, arg2) function_to_decorate(arg1, arg2) return a_wrapper_accepting_arguments # Теперь, когда мы вызываем функцию, которую возвращает декоратор, мы вызываем её "обёртку", # передаём ей аргументы и уже в свою очередь она передаёт их декорируемой функции @a_decorator_passing_arguments def print_full_name(first_name, last_name): print("Меня зовут", first_name, last_name) print_full_name("Vasya", "Pupkin")
А теперь попробуем написать декоратор, принимающий аргументы:
def decorator_maker(): print("Я создаю декораторы! Я буду вызван только раз: когда ты попросишь меня создать декоратор.") def my_decorator(func): print("Я - декоратор! Я буду вызван только раз: в момент декорирования функции.") def wrapped(): print ("Я - обёртка вокруг декорируемой функции.\n" "Я буду вызвана каждый раз, когда ты вызываешь декорируемую функцию.\n" "Я возвращаю результат работы декорируемой функции.") return func() print("Я возвращаю обёрнутую функцию.") return wrapped print("Я возвращаю декоратор.") return my_decorator # Давайте теперь создадим декоратор. Это всего лишь ещё один вызов функции new_decorator = decorator_maker() # Теперь декорируем функцию def decorated_function(): print("Я - декорируемая функция.") decorated_function = new_decorator(decorated_function) # Теперь наконец вызовем функцию: decorated_function()
Теперь перепишем данный код с помощью декораторов:
@decorator_maker() def decorated_function(): print("Я - декорируемая функция.") decorated_function()
Вернёмся к аргументам декораторов, ведь, если мы используем функцию, чтобы создавать декораторы «на лету», мы можем передавать ей любые аргументы, верно?
def decorator_maker_with_arguments(decorator_arg1, decorator_arg2): print("Я создаю декораторы! И я получил следующие аргументы:", decorator_arg1, decorator_arg2) def my_decorator(func): print("Я - декоратор. И ты всё же смог передать мне эти аргументы:", decorator_arg1, decorator_arg2) # Не перепутайте аргументы декораторов с аргументами функций! def wrapped(function_arg1, function_arg2): print ("Я - обёртка вокруг декорируемой функции.\n" "И я имею доступ ко всем аргументам\n" "\t- и декоратора: \n" "\t- и функции: \n" "Теперь я могу передать нужные аргументы дальше" .format(decorator_arg1, decorator_arg2, function_arg1, function_arg2)) return func(function_arg1, function_arg2) return wrapped return my_decorator @decorator_maker_with_arguments("Леонард", "Шелдон") def decorated_function_with_arguments(function_arg1, function_arg2): print ("Я - декорируемая функция и я знаю только о своих аргументах: " " ".format(function_arg1, function_arg2)) decorated_function_with_arguments("Раджеш", "Говард")
Таким образом, мы можем передавать декоратору любые аргументы, как обычной функции.
- Декораторы несколько замедляют вызов функции, не забывайте об этом.
- Вы не можете «раздекорировать» функцию. Безусловно, существуют трюки, позволяющие создать декоратор, который можно отсоединить от функции, но это плохая практика. Правильнее будет запомнить, что если функция декорирована — это не отменить.
- Декораторы оборачивают функции, что может затруднить отладку.
Упражнение 3
Напишите декоратор swap, который делает так, что задекорированная функция принимает все свои неименованные аргументы в порядке, обратном тому, в котором их передали (для аргументов с именем не вполне правильно учитывать порядок, в котором они были переданы).
Пример ожидаемого поведения:
@swap def div(x, y, show=False): res = x / y if show: print(res) return res div(2, 4, show=True)
Упражнение 4
- Время вызова функции
- Входящие аргументы
- Ответ return (если есть, если нет то логгировать ‘-‘)
- Время завершения работы функции
- Время работы функции
Сайт построен с использованием Pelican. За основу оформления взята тема от Smashing Magazine. Исходные тексты программ, приведённые на этом сайте, распространяются под лицензией GPLv3, все остальные материалы сайта распространяются под лицензией CC-BY.
Пишем инструменты командной строки на Python с помощью Click
Интерфейсы командной строки — эффективная вещь, так как они позволяют автоматизировать практически всё что угодно. Сегодня мы расскажем, как написать такой интерфейс на Python с помощью Click.
Python — невероятно гибкий язык программирования, который хорошо интегрируется с существующими программами. Немало Python-кода написано в виде скриптов и интерфейсов командной строки (CLI).
Инструменты и интерфейсы командной строки — эффективная вещь, так как они позволяют автоматизировать практически всё что угодно. Как следствие, эти интерфейсы с течением времени могут стать довольно сложными.
Обычно всё начинается с простого скрипта на Python, который что-то делает. Например, получает доступ к веб-API и выводит результат в консоль:
# print_user_agent.py import requests json = requests.get('http://httpbin.org/user-agent').json() print(json['user-agent'])Вы можете запустить этот скрипт с помощью команды python3 print_user_agent.py , и он выведет имя user-agent, использованного для вызова API.
Как и было сказано, довольно простой скрипт.
Но что делать, когда подобная программа растёт и становится всё более сложной?
Решением этого вопроса мы сегодня и займёмся. Вы узнаете об основах написания интерфейсов командной строки на Python и о том, как click позволяет упростить этот процесс.
Используя эти знания, мы шаг за шагом перейдём от простого скрипта к интерфейсу командной строки с аргументами, опциями и полезными инструкциями по использованию. Всё это мы сделаем с помощью библиотеки click.
К концу этой статьи вы будете знать:
- Почему click — лучшая альтернатива argparse и optparse;
- Как с его помощью создать простой CLI;
- Как добавить обязательные аргументы командной строки в ваши скрипты;
- Как парсить флаги и опции командной строки;
- Как сделать ваши консольные приложения более удобными, добавив справочный текст.
Вы увидите, как сделать всё это с минимальным количеством шаблонного кода.
Примечание переводчика Код в данной статье написан на Python 3.6, работоспособность на более ранних версиях не гарантируется.
Зачем вам писать скрипты и инструменты для командной строки на Python?
Код выше — всего лишь пример, не очень полезный в реальной жизни. На самом деле скрипты бывают куда более сложные. Возможно, вы имели опыт с ними и знаете, что они могут быть важной частью нашей повседневной работы: некоторые скрипты остаются на протяжении всего времени жизни проекта, для которого они были написаны. Некоторые начинают приносить пользу другим командам или проектам. У них даже может расширяться функционал.
В этих случаях важно сделать скрипты более гибкими и настраиваемыми с помощью параметров командной строки. Они позволяют указать имя сервера, учётные данные или любую другую информацию скрипту.
Здесь приходят на выручку такие модули, как optparse и argparse, которые делают нашу жизнь на порядок проще. Но прежде чем мы с ними познакомимся, давайте разберёмся с терминологией.
Основы интерфейса командной строки
Интерфейс командной строки (CLI) начинается с имени исполняемого файла. Вы вводите имя в консоль и получаете доступ к главной точке входа скрипта, такого как pip.
В зависимости от сложности CLI обычно есть определённые параметры, которые вы можете передавать скрипту:
- Аргумент, который является обязательным параметром. Если его не передать, то CLI вернёт ошибку. Например, в следующей команде click является аргументом: pip install click .
- Опция — необязательный параметр, который объединяет имя и значение, например —cache-dir ./my-cache . Вы говорите CLI, что значение ./my-cache должно использоваться как директория для кэша.
- Флаг, который включает или выключает определённый сценарий. Вероятно, самым частым является —help . Вы только указываете имя, а CLI самостоятельно интерпретирует значение.
С более сложными CLI, такими как pip или Heroku CLI, вы получаете доступ к набору функций, которые собраны под главной точкой входа. Они обычно называются командами или подкомандами.
Возможно, вы уже использовали CLI, когда устанавливали Python-библиотеку с помощью команды pip install . Команда install говорит CLI, что вы хотите использовать функцию установки пакета, и даёт вам доступ к параметрам, характерным для этой функции.
Пакеты для работы с командной строкой, доступные в стандартной библиотеке Python 3.x
Добавление команд и параметров в ваши скрипты может сделать их значительно лучше, но парсить командную строку не так просто, как может показаться. Однако вместо того, чтобы пытаться самостоятельно решить эту проблему, лучше воспользоваться одним из многих пакетов, которые сделали это за вас.
Два наиболее известных пакета для этого — optparse и argparse. Они являются частью стандартной библиотеки Python и добавлены туда по принципу «всё включено».
По большей части они делают одно и то же и работают схожим образом. Главное отличие заключается в том, что optparse не используется начиная с Python 3.2, и argparse считается стандартом для создания CLI в Python.
Вы можете узнать о них больше в документации Python, но, чтобы иметь представление, как выглядит скрипт с argparse, посмотрите на пример ниже:
import argparse parser = argparse.ArgumentParser(description='Process some integers.') parser.add_argument('integers', metavar='N', type=int, nargs='+', help='an integer for the accumulator') parser.add_argument('--sum', dest='accumulate', action='store_const', const=sum, default=max, help='sum the integers (default: find the max)') args = parser.parse_args() print(args.accumulate(args.integers))click против argparse: лучшая альтернатива?
Вероятно, вы смотрите на этот код и думаете: «Что это всё значит?» И это является одной из проблем argparse: код с ним неинтуитивен и сложночитаем.
Поэтому вам может понравиться click.
Click решает ту же проблему, что и optparse и argparse, но немного иначе. Он использует декораторы, поэтому ваши команды должны быть функциями, которые можно обернуть этими декораторами.
С click легко создавать многофункциональный CLI с небольшим количеством кода. И этот код будет легко читаться, даже когда ваш CLI вырастет и станет более сложным.
Пишем простой CLI на Python с помощью click
Вдоволь поговорив о CLI и библиотеках, давайте взглянем на пример, чтобы понять, как написать простой CLI с click. Как и в первом примере, мы создаём простой CLI, который выводит результат в консоль. Это несложно:
# cli.py import click @click.command() def main(): print("I'm a beautiful CLI ✨") if __name__ == "__main__": main()Не пугайтесь последних двух строк: это то, как Python запускает функцию main при исполнении файла как скрипта.
Как вы видите, всё, что нам нужно сделать — создать функцию и добавить к ней декоратор @click.command() . Он превращает функцию в команду, которая является главной точкой входа нашего скрипта. Теперь вы можете запустить скрипт через командную строку и увидеть что-то вроде этого:
$ python3 cli.py I'm a beautiful CLI ✨Что в click здорово, так это то, что мы получаем некоторые дополнительные возможности просто так. Мы не реализовывали справочную функцию, однако вы можете добавить флаг —help и увидеть базовое сообщение:
$ python3 cli.py --help Usage: cli.py [OPTIONS] Options: --help Show this message and exit.Более реалистичный пример CLI на Python с использованием click
Теперь, когда вы знаете, как click упрощает написание CLI, давайте взглянем на более реалистичный пример. Мы напишем программу, которая позволяет нам взаимодействовать с веб-API.
API, который мы дальше будем использовать, — OpenWeatherMap API. Он предоставляет информацию о текущей погоде, а также прогноз на пять дней для определённого местоположения. Мы начнём с тестового API, который возвращает текущую погоду для места.
Прежде чем мы начнём писать код, давайте познакомимся с API. Для этого можно использовать сервис HTTPie, включая онлайн-терминал.
Давайте посмотрим, что случится, когда мы обратимся к API с Лондоном в качестве местоположения:
$ http --body GET http://samples.openweathermap.org/data/2.5/weather \ q==London \ appid==b1b15e88fa797225412429c1c50c122a1 < "base": "stations", "clouds": < "all": 90 >, "cod": 200, "coord": < "lat": 51.51, "lon": -0.13 >, "dt": 1485789600, "id": 2643743, "main": < "humidity": 81, "pressure": 1012, "temp": 280.32, "temp_max": 281.15, "temp_min": 279.15 >, "name": "London", "sys": < "country": "GB", "id": 5091, "message": 0.0103, "sunrise": 1485762037, "sunset": 1485794875, "type": 1 >, "visibility": 10000, "weather": [ < "description": "light intensity drizzle", "icon": "09d", "id": 300, "main": "Drizzle" >], "wind": < "deg": 80, "speed": 4.1 >>Если вы смущены наличием API-ключа в примере сверху, не переживайте, это тестовый API-ключ, предоставляемый сервисом.
Более важное наблюдение заключается в том, что мы отправляем два параметра (обозначаемые == при использовании HTTPie), чтобы узнать текущую погоду:
- q — место, в котором мы хотим узнать погоду;
- appid — наш API-ключ.
Это позволяет нам создать простую реализацию на Python с использованием библиотеки requests (опустим обработку ошибок и неудачных запросов для простоты):
import requests SAMPLE_API_KEY = 'b1b15e88fa797225412429c1c50c122a1' def current_weather(location, api_key=SAMPLE_API_KEY): url = 'http://samples.openweathermap.org/data/2.5/weather' query_params = < 'q': location, 'appid': api_key, >response = requests.get(url, params=query_params) return response.json()['weather'][0]['description']Эта функция делает простой запрос к API, используя два параметра. В качестве обязательного аргумента она принимает location (местоположение), которое должно быть строкой. Также мы можем указать API-ключ, передавая параметр api_key при вызове функции. Это необязательно, так как по умолчанию используется тестовый ключ.
И вот мы видим текущую погоду в Python REPL:
>>> current_weather('London') 'light intensity drizzle' # впрочем, ничего нового ?Парсим обязательные параметры с click
Простая функция current_weather позволяет нам создать CLI с местоположением, указанным пользователем. Это должно работать примерно так:
$ python3 cli.py London The weather in London right now: light intensity drizzle.Как вы, возможно, догадались, местоположение — это аргумент, поскольку оно является обязательным параметром для нашего погодного CLI.
Как нам сделать это при помощи click? Всё довольно просто, мы используем декоратор под названием argument . Кто бы мог подумать?
Давайте возьмём наш предыдущий пример и слегка изменим его, добавив аргумент location :
@click.command() @click.argument('location') def main(location): weather = current_weather(location) print(f"The weather in right now: .")Если этот print выглядит для вас странно, не волнуйтесь — это новый способ форматирования строк в Python 3.6+, который называется f-форматированием.
Как вы видите, всё, что нам нужно сделать, это добавить дополнительный декоратор к нашей функции main и дать ему имя. Click использует имя в качестве имени аргумента, переданного обёрнутой функции.
Примечание переводчика Имя аргумента, переданное click, должно совпадать с именем аргумента в объявлении функции.
В нашем случае значение аргумента командной строки location будет передано функции main в качестве аргумента location . Логично, не так ли?
Также вы можете использовать тире в именах, например api-key , которые click переведёт в snake case для имени аргумента в функции, например main(api_key) .
Реализация main просто использует нашу функцию current_weather для получения погоды в указанном месте. И затем мы с помощью print выводим полученную информацию.
Парсим опциональные параметры с click
Как вы, возможно, догадались, тестовый API ограничивает нас в возможностях. Поэтому, прежде чем мы продолжим, зарегистрируйтесь и получите настоящий API-ключ.
Первое, что нам нужно изменить, — URL, откуда берутся данные о текущей погоде. Это можно сделать, изменив значение переменной url в функции current_weather на URL, указанный в документации OpenWeatherMap:
def current_weather(location, api_key=SAMPLE_API_KEY): url = 'https://api.openweathermap.org/data/2.5/weather' # дальше всё остаётся как было .Это изменение приведёт к неработоспособности нашего CLI, так как указанный API-ключ не работает с реальным API. Поэтому давайте добавим новый параметр в наш CLI, который позволит нам указывать API-ключ. Но сначала мы должны решить, будет ли этот параметр аргументом или опцией. Мы сделаем его опцией, так как добавление параметра вроде —api-key делает его более явным и говорящим за себя.
Мы хотим, чтобы наша программа запускалась таким образом:$ python3 cli.py --api-key London The weather in London right now: light intensity drizzle.Проще простого. Посмотрим, как добавить опцию к нашей существующей команде:
@click.command() @click.argument('location') @click.option('--api-key', '-a') def main(location, api_key): weather = current_weather(location, api_key) print(f"The weather in right now: .")И снова мы добавляем декоратор к нашей функции main . В этот раз мы используем декоратор с говорящим именем @click.option и указываем имя для нашей опции, начинающееся с двух тире. Как вы видите, мы также можем указать сокращение для нашей опции с одним тире, чтобы сэкономить пользователю немного времени.
Как было сказано ранее, click создаёт аргумент для передачи в функцию main из длинного варианта имени. В случае с опцией он убирает впередистоящие тире и переводит её в snake case. Таким образом, —api-key становится api_key .
Чтобы всё заработало, осталось лишь передать API-ключ в функцию current_weather .
Мы добавили возможность указывать свой собственный ключ и проверять погоду в любом месте:
$ python3 cli.py --api-key Canmore The weather in Canmore right now: broken clouds.Добавляем автоматически генерируемые инструкции по использованию
Можете себя похвалить, вы создали отличный небольшой CLI почти без шаблонного кода. Однако прежде чем вы решите отдохнуть, давайте убедимся, что новый пользователь будет знать, как пользоваться нашим CLI, путём добавления документации. Не бойтесь, всё будет просто.
Сначала давайте проверим, что выведет флаг —help после всех сделанных изменений. Довольно неплохо, учитывая что мы не приложили к этому никаких усилий:
$ python3 cli.py --help Usage: cli.py [OPTIONS] LOCATION Options: -a, --api-key TEXT --help Show this message and exit.Первое, что нужно исправить, это добавить описание для нашей опции с API-ключом. Всё, что нам для этого нужно сделать, — добавить справочный текст в декоратор @click.option :
@click.command() @click.argument('location') @click.option( '--api-key', '-a', help='your API key for the OpenWeatherMap API', ) def main(location, api_key): .Второе (и последнее), что мы сделаем, — добавим документацию для всей click-команды. Самый простой и самый питонический способ сделать это — добавить строку документации в нашу функцию main . Да, нам в любом случае нужно сделать это, поэтому это не лишняя работа:
. def main(location, api_key): """ A little weather tool that shows you the current weather in a LOCATION of your choice. Provide the city name and optionally a two-digit country code. Here are two examples: 1. London,UK 2. Canmore You need a valid API key from OpenWeatherMap for the tool to work. You can sign up for a free account at https://openweathermap.org/appid. """ .Сложив всё вместе, мы получаем хороший вывод для нашего инструмента:
$ python3 cli.py --help Usage: cli.py [OPTIONS] LOCATION A little weather tool that shows you the current weather in a LOCATION of your choice. Provide the city name and optionally a two-digit country code. Here are two examples: 1. London,UK 2. Canmore You need a valid API key from OpenWeatherMap for the tool to work. You can sign up for a free account at https://openweathermap.org/appid. Options: -a, --api-key TEXT your API key for the OpenWeatherMap API --help Show this message and exit.Подводим итоги
Итак, в этом уроке мы рассмотрели много всего. Можете гордиться собой, вы написали свой собственный CLI, и всё это с минимальным количеством шаблонного кода! Исходный код ниже доказывает это. Не стесняйтесь использовать его для собственных экспериментов:
import click import requests SAMPLE_API_KEY = 'b1b15e88fa797225412429c1c50c122a1' def current_weather(location, api_key=SAMPLE_API_KEY): url = 'https://api.openweathermap.org/data/2.5/weather' query_params = < 'q': location, 'appid': api_key, >response = requests.get(url, params=query_params) return response.json()['weather'][0]['description'] @click.command() @click.argument('location') @click.option( '--api-key', '-a', help='your API key for the OpenWeatherMap API', ) def main(location, api_key): """ A little weather tool that shows you the current weather in a LOCATION of your choice. Provide the city name and optionally a two-digit country code. Here are two examples: 1. London,UK 2. Canmore You need a valid API key from OpenWeatherMap for the tool to work. You can sign up for a free account at https://openweathermap.org/appid. """ weather = current_weather(location, api_key) print(f"The weather in right now: .") if __name__ == "__main__": main()Как написать утилиту командной строки?
Нужно написать консольную утилиту converter.py, поддерживающую аргументы командной строки.
python convert.py [--csv2parquet | --parquet2csv ] | [--get-schema ] | [--help]Я могу написать python файл где это все будет выполняться, но как сделать из этого файла утилиту командной строки?
То есть, я знаю python и библиотеку pandas, но как написать консольную утилиту?
Что по шагам мне нужно сделать, чтобы решить это задание?- Вопрос задан более двух лет назад
- 564 просмотра
