Автоматизируем аргументы командной строки на Python с Google Fire

fire

Сегодня мы рассмотрим библиотеку Fire, которая позволяет автоматизировать интерфейсы командной строки (CLI) на Python.

Fire – самая популярная после TensorFlow разработка для Python, которую Google выложил в открытый доступ:

FireFire

Правда, она не настолько популярна…

Что интересно в библиотеке TensorFlow, так это то, что на момент её выхода на рынке уже было несколько популярных библиотек глубокого обучения (Theano, Caffe и Torch). Несмотря на это, TensorFlow сразу начал набирать популярность, посмотрите на активность Stack Overflow:

Fire

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

Примечание После использования Fire в течение нескольких недель и сравнения с другими вариантами вроде Click, становится понятно, что Fire больше подходит для быстрой разработки, так как в нём отсутствуют многие функции других CLI. Тем не менее, новые функции находятся в разработке. В первом релизе IPython был обязательной зависимостью, что приводило к долгой загрузке даже простых скриптов, но сейчас в нём нет необходимости!

Пример приложения с Fire

Fire можно установить с помощью pip:

pip install fire

Документацию можно найти на GitHub. В ней содержится проницательная заметка:

Почему библиотека называется Fire?

Когда вы вызываете Fire, она выстреливает (fire off, выполняет) вашей командой.

Мы можем выполнить команду, вызвав fire.Fire(my_object), где my_object может быть функцией, классом или другим объектом.

Теперь пора делать приложение. Мы напишем генератор случайного выбора. Функция random_sample будет вычисляться через Fire. Как мы увидим, значения, возвращённые функцией, будут выведены в консоль.

Посмотрим на код:

'''
Функция для генерации случайного выбора.
Использование:
Для получения инструкций по использованию введите следующую команду:
>>> python3 random_sample.py -- --help
'''
import fire
import numpy as np

def random_sample(choices, num_samples=1, seed=None):
    '''
    Возвращает случайный выбор из входных данных.
    
    choices : tuple
        Варианты для выбора.
    num_samples : int
        Количество возвращаемых значений.
    seed : int
        Переменная для инициализации генератора случайных чисел. Используйте None для получения случайного значения.
    '''
    num_samples = int(num_samples)
    seed = int(seed) if str(seed).isnumeric() else seed

    print(f'Варианты для выбора: {repr(choices)}')
    print(f'Выбираю {num_samples} значений')
    print(f'Seed: {seed}')
    np.random.seed(seed)
    samples = np.random.choice(choices, size=num_samples)
    print('Результат:')
    return '\n'.join(samples)

def main():
    fire.Fire(random_sample)

if __name__ == '__main__':
    main()

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

Мы передаём нашу функцию random_sample методу Fire, который автоматически создаёт аргументы командной строки для параметров функции. Нам не нужно писать инструкции по применению скрипта: пользователь может ввести python3 random_sample.py -- --help и увидеть следующий вывод:

$ python3 random_sample.py – –help
Type:         function
String form:  <function random_sample at 0x1003bcf28> 
File:         ~/Documents/blog-posts/google-fire/random-sample/random_sample.py
Line:         10
Docstring:    Возвращает случайный выбор из входных данных. choices : tuple Варианты для выбора. num_samples : int Количество возвращаемых значений. seed : int Переменная для инициализации генератора случайных чисел. Используйте None для получения случайного значения. Usage: random_sample.py CHOICES [NUM_SAMPLES] [SEED] random_sample.py --choices CHOICES [--num-samples NUM_SAMPLES] [--seed SEED]

Пример использования скрипта:

>>> python3 random_sample.py 'Bohr, Schrödinger, Einstein, Dirac' --num-samples 4 --seed 1905
Варианты для выбора: ('Bohr', 'Schrödinger', 'Einstein', 'Dirac')
Выбираю 4 значений
Seed: 1905
Результат:
Dirac
Einstein
Dirac
Schrödinger

Примечание Если вы хотите запускать скрипт как исполняемый файл (например, ./random_sample.py вместо python3 random_sample.py), то добавьте в начале скрипта строку #!/usr/bin/env python и разрешите исполнение скрипта с помощью команды chmod +x random_sample.py.

Интерактивный режим

Что в Fire интересно, так это то, что он может перенести вас в среду IPython REPL. Для этого вам просто нужно добавить -- --interactive в конце вызова вашего скрипта. Например:

$ python3 random_sample.py 'Bohr, Schrödinger, Einstein, Dirac' --num-samples 4 --seed 1905 -- --interactive
Варианты для выбора: ('Bohr', 'Schrödinger', 'Einstein', 'Dirac') 
Выбираю 4 значений 
Seed: 1905 
Результат: 
Fire is starting a Python REPL with the following objects: 
Modules: fire, np 
Objects: component, main, random_sample.py, random_sample, result, self, trace 

Python 3.5.2 |Anaconda 4.2.0 (x86_64)| (default, Jul 2 2016, 17:52:12) 
Type "copyrigth", "credits" or "license" for more information. 

IPython 5.1.0 -- An enhanced Interactive Python. 
? -> Introduction and overview of IPython's features 
%quickref -> Quick reference. 
help -> Python's own help system. 
object -> Details about 'object', use 'object??' for extra details. 

In [1]: print(result) 
Dirac 
Einstein 
Dirac 
Schrödinger
  1. Запускаем random_sample.py.
  2. Показываем вывод random_sample.py.
  3. Заметили модуль и объекты, которые были загружены?
  4. Запускаем интерпретатор IPython.
  5. Выводим результат.

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

IPython 5.1.0 -- An enhanced Interactive Python.
?         -> Introduction and overview of IPython's features
%quickref -> Quick reference.
help      -> Python's own help system.
object    -> Details about 'object', use 'object??' for extra details.

In [1]: print(result)
Dirac
Einstein
Dirac
Schrödinger

In [2]: print(random_sample.__doc__)

     Возвращает случайный выбор из входных данных. 
     choices : str 
         Варианты для выбора. 
     num_samples : int 
         Количество возвращаемых значений. 
     seed : int          
         Переменная для инициализации генератора случайных чисел. Используйте None для получения случайного значения. 

In [3]: sample_2 = random_sample(('Bohr', 'Schrödinger'), num_samples=2, seed=3) 
Варианты для выбора: ('Bohr', 'Schrödinger') 
Выбираю 2 значений 
Seed: 3 
Результат: 

In [4]: print(sample_2) 
Bohr 
Bohr

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

>>> python3 random_sample.py 'Bohr, Schrödinger' --num-samples 2 --seed 3

Зажигаем класс

Вы можете передать любой объект модулю Fire, и библиотека сгенерирует CLI. Вот пример, в котором мы используем класс:

'''
Класс для получения данных с вики-страницы.
Для получения инструкций по использованию введите следующие команды:
>>> python3 wiki_page.py -- --help
>>> python3 wiki_page.py get-html-element -- --help
'''
import fire
import requests
import re

class WikiPage():
    '''
    Запрашивает страницу из Википедии.
    
    page : str
        URL страницы. По умолчанию возвращает случайную страницу.
    '''
    def __init__(self, page='https://en.wikipedia.org/wiki/Special:Random'):
        page = requests.get(page)
        self.page = page
        self.url = page.url
        self.status_code = page.status_code

    def get_html_element(self, element):
        ''' Получить HTML-тег вроде title, h1, p, etc... '''
        try:
            # Пытаемся достать тег с помощью регулярного выражения.
            text = re.findall(r'<{name}[^>]*>(.*)(?:</{name})'.format(name=element),
                              self.page.text)[0]
        except Exception as e:
            print(f'Не могу найти тег {element} на странице' )
            raise e
        return text

def main():
    fire.Fire(WikiPage)

if __name__ == '__main__':
    main()

Наш класс WikiPage запрашивает страницу из Википедии, и затем функция get_html_element ищет HTML-элемент на этой странице.

Примечание Для поиска HTML-элементов лучше использовать специальные библиотеки вроде BeautifulSoup, а не регулярные выражения.

Давайте посмотрим, как это работает:

$ python3 wiki_page.py -- --help
Type:         type
String form:  <class '__main__.WikiPage'> 
File:         ~/Documents/blog-posts/google-fire/get-wiki-page/wiki_page.py
Line:         11
Docstring:    Запрашивает страницу из Википедии.

page : str
        URL страницы. Ро умолчанию возвращает случайную страницу.

Usage:           wiki_page.py [PAGE] 
                 wiki_page.py [--page PAGE]

При передаче аргумента page мы получаем полезную инструкцию по использованию:

$ python3 wiki_page.py --page https://en.wikipedia.org/wiki/History_of_quantum_mechanics
Type:         WikiPage
String form:  <__main__.WikiPage object at 0x104dd0630> 
File:         ~/Documents/blog-posts/google-fire/get-wiki-page/wiki_page.py
Docstring:    Запрашивает страницу из Википедии.

page : str
        URL страницы. По умолчанию возвращает случайную страницу.

Usage:           wiki_page.py --page https://en.wikipedia.org/wiki/History_of_quantum_mechanics 
                 wiki_page.py --page https://en.wikipedia.org/wiki/History_of_quantum_mechanics get-html-element
                 wiki_page.py --page https://en.wikipedia.org/wiki/History_of_quantum_mechanics page
                 wiki_page.py --page https://en.wikipedia.org/wiki/History_of_quantum_mechanics status-code
                 wiki_page.py --page https://en.wikipedia.org/wiki/History_of_quantum_mechanics url

Давайте попробуем получить код состояния:

>>> python3 wiki_page.py --page https://en.wikipedia.org/wiki/History_of_quantum_mechanics status-code 200

А теперь сгенерируем случайную страницу и проверим URL:

>>> python3 wiki_page.py url 
https://en.wikipedia.org/wiki/Parasicydium_bandama

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

$ python3 wiki_page.py get-html-element -- --help
Type:         WikiPage
String form:  <bound method WikiPage.get_html_elememt of <__main__.WikiPage object at 0x104d555c0>> 
File:         ~/Documents/blog-posts/google-fire/get-wiki-page/wiki_page.py
LineL         24
Docstring:    Получить HTML-тег вроде title, h1, p, etc...

page : str
        URL страницы. По умолчанию возвращает случайную страницу.

Usage:           wiki_page.py get-html-element ELEMENT
                 wiki_page.py get-html-element ELEMENT

Например:

>>> python3 wiki_page.py --page https://en.wikipedia.org/wiki/History_of_quantum_mechanics get-html-element title
History of quantum mechanics - Wikipedia

Перевод статьи «Automated python arguments with Google Fire»

Ещё интересное для вас:
Тест: чьё это рабочее место? Угадываем айтишников по их столам
Тест: что вы знаете о работе мозга?
Тест: какой язык программирования вам стоит выбрать для изучения?