Если вы занимаетесь обработкой и анализом данных с использованием Python, то вам, рано или поздно, придётся выйти за пределы Jupyter Notebook, преобразовав свой код в скрипты, которые можно запускать средствами командной строки. Здесь вам и пригодится модуль argparse. Для новичков, привыкших к Jupyter Notebook, такой шаг означает необходимость покинуть зону комфорта и перейти в новую среду. Материал, перевод которого мы публикуем сегодня, написан для того, чтобы облегчить подобный переход.


Модуль argparse

Модуль argparse


Модуль argparse можно сравнить с силами природы, которые воздвигли горные пики, возвышающиеся над облаками. Благодаря этому модулю в скриптах становится возможным работа с тем, что, без его использования, было бы скрыто от кода этих скриптов.

Надо отметить, что argparse является рекомендуемым к использованию модулем стандартной библиотеки Python, предназначенным для работы с аргументами командной строки. Мне не удалось найти хорошее руководство по argparse для начинающих, поэтому я и решил написать такое руководство сам.

Жизнь за пределами Jupyter Notebook


Когда я впервые столкнулся с argparse в Python-скрипте, который нужен был мне для проекта, которым я занимался в свободное время, я подумал: «А это что ещё за таинственная конструкция?». После этого я быстро перенёс код в Jupyter Notebook, но такой ход оказался нерациональным.

Мне нужно было, чтобы у меня была возможность просто запустить скрипт, а не работать с ним средствами Jupyter Notebook. Автономным скриптом, в котором использовался модуль argparse, было бы гораздо легче пользоваться, работать над ним было бы проще, чем полагаясь на возможности Jupyter Notebook. Однако тогда я спешил, и, когда взглянул на документацию по argparse, не смог сходу ухватить её суть, поэтому и не стал пользоваться исходной версией скрипта.

С тех пор я разобрался с argparse и этот модуль мне очень понравился. Теперь я считаю его прямо-таки жизненно необходимым. При этом освоить его не так уж и сложно.

Зачем нужен модуль argparse?


Модуль argparse позволяет разбирать аргументы, передаваемые скрипту при его запуске из командной строки, и даёт возможность пользоваться этими аргументами в скрипте. То есть речь идёт о том, что этот модуль позволяет предоставлять скрипту некие данные в момент его запуска, а этими данными скрипт сможет воспользоваться во время выполнения его кода. Модуль argparse — это средство, с помощью которого можно наладить общение между автором программы и тем, кто ей пользуется, например — между вами, когда вы сегодня пишете скрипт, и вами же, когда вы завтра его запускаете, что-то ему передавая.

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

Пример


Предположим, вы хотите написать скрипт для преобразования видеофайлов в обычные изображения с использованием библиотеки OpenCV. Для того чтобы скрипт мог бы решить эту задачу, ему нужно знать место, где хранятся видеофайлы, и место, в которое нужно поместить готовые изображения. То есть, ему требуются сведения о двух папках, пути к которым, что не очень удобно, можно жёстко задать в коде скрипта, или, что уже куда лучше, можно позволить задавать пользователю скрипта, вводя их в качестве аргументов командной строки при запуске скрипта. Для того чтобы оснастить скрипт такой возможностью, нам и пригодится модуль argparse. Вот как может выглядеть раздел скрипта (назовём этот скрипт videos.py), в котором осуществляется разбор аргументов командной строки:

# videos.py
import argparse
parser = argparse.ArgumentParser(description='Videos to images')
parser.add_argument('indir', type=str, help='Input dir for videos')
parser.add_argument('outdir', type=str, help='Output dir for image')
args = parser.parse_args()
print(args.indir)

Здесь, в начале файла, импортируется модуль argparse. Затем, с использованием конструкции argparse.ArgumentParser(), создаётся объект parser с указанием его описания. Далее, с помощью метода parser.add_argument(), описывается переменная indir, в которую планируется записать путь к папке с видеофайлами. При этом указывается то, что она имеет строковой тип, а также задаётся справочная информация о ней. После этого, точно так же, создаётся переменная outdir, в которую попадёт путь к папке, в которую скрипт должен будет поместить изображения, созданные на основе видеофайлов. На следующем шаге работы в переменную args попадает результат разбора аргументов командной строки. То, что передано скрипту при запуске, теперь будет доступно в виде свойств indir и outdir объекта args. Теперь с этими значениями можно работать. В данном случае мы просто выводим в консоль то, что передано скрипту в аргументе indir.

Вот как запустить этот скрипт из командной строки:

python videos.py /videos /images

Обратите внимание на то, что строки /videos и /images не нужно заключать в кавычки. Скрипт, запущенный таким образом, выведет в терминал строку /videos, чем подтвердит возможность использования переданных ему аргументов в своём коде. Это — магия argparse в действии.


Магия разбора аргументов командной строки

Подробности об argparse


Только что мы рассмотрели простой пример работы с argparse. Теперь давайте обсудим некоторые подробности, касающиеся argparse.

?Позиционные аргументы


Конструкция вида parser.add_argument('indir', type=str, help='Input dir for videos') из скрипта videos.py предназначена для создания позиционного аргумента (positional argument). При вызове скрипта важен порядок указания таких аргументов. Так, первый аргумент, переданный скрипту, становится первым позиционным аргументом, второй аргумент — вторым позиционным аргументом.

Что произойдёт в том случае, если скрипт запустить вообще без аргументов, выполнив в терминале команду python videos.py?

В таком случае будет выведено сообщение об ошибке следующего вида:

videos.py: error: the following arguments are required: indir, outdir

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

?Необязательные аргументы


Что произойдёт при запуске нашего скрипта командой python videos.py --help?

В ответ будет выведена справочная информация о нём. Это — именно те сведения о позиционных аргументах, которые мы указывали при описании соответствующих переменных:

usage: videos.py [-h] indir outdir

Videos to images

positional arguments:
  indir       Input dir for videos
  outdir      Output dir for image

optional arguments:
  -h, --help  show this help message and exit

Скрипт сообщил нам много интересного о том, чего он ждёт от пользователя, а help — это пример необязательного аргумента (optional argument). Обратите внимание на то, что --help (или -h) — это единственный стандартный необязательный аргумент, которым мы можем пользоваться при работе с argparse, но, если вам нужны и другие необязательные аргументы, их можно создавать и самостоятельно.

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

parser.add_argument('-m', '--my_optional')

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

# my_example.py
import argparse
parser = argparse.ArgumentParser(description='My example explanation')
parser.add_argument(
    '--my_optional',
    type=int,
    default=2,
    help='provide an integer (default: 2)'
)
my_namespace = parser.parse_args()
print(my_namespace.my_optional)

Аргумент, описанный как --my_optional, доступен в программе в виде свойства объекта my_namespace с именем my_optional.

Необязательным аргументам можно назначать значения, которые они будут иметь по умолчанию. В нашем случае, если при вызове скрипта аргументу my_example не будет задано никакого значения, в него будет записано число 2, которое и будет выведено в консоль. Для того чтобы задать значение этого аргумента во время запуска скрипта можно воспользоваться такой конструкцией:

python my_example.py  --my_optional=3

Для чего ещё можно использовать argparse?


Модуль argparse можно использовать при разработке Python-приложений, которые планируется упаковывать в контейнеры Docker. Так, например, если при запуске приложения, упакованного в контейнер, ему нужно передать аргументы командной строки, то описать это, на этапе сборки контейнера, можно в Dockerfile с помощью инструкции RUN. Для запуска скриптов во время выполнения контейнера можно пользоваться инструкциями CMD или ENTRYPOINT. Подробности о файлах Dockerfile вы можете найти здесь.

Итоги


Мы рассмотрели базовые способы работы с модулем argparse, используя которые, вы можете оснастить свои скрипты возможностью принимать и обрабатывать аргументы командной строки. При этом надо отметить, что возможности argparse на этом не заканчиваются. Например, использование при описании аргументов параметра nargs позволяет работать со списками аргументов, а параметр choices позволяет задавать наборы значений, которые могут принимать аргументы. На самом деле, теперь, освоив основные возможности argparse, вы, без особых сложностей, сможете изучить этот модуль более глубоко, используя документацию к нему.

Если вы привыкли работать с Jupyter Notebook и хотите отойти от этой практики, то вот и вот — материалы по работе с переменными окружениями. Вот материал, посвящённый средству, repo2docker, позволяющему преобразовывать репозитории Jupyter Notebook в образы Docker.

Уважаемые читатели! Как вы работаете с аргументами командной строки в Python-скриптах?

Комментарии (9)


  1. site6893
    18.02.2019 14:21
    +3

    Мне не удалось найти хорошее руководство по argparse для начинающих, поэтому я и решил написать такое руководство сам.

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


  1. nvmalovichko
    18.02.2019 16:41
    +2

    Спасибо за статью!

    Занимаюсь ETL и анализом данных, разработчик. Сначала активно использовали argparse, потом мигрировали на Flask-Script. Сейчас комбинируем его с click.

    Преимущества миграции на Flask-Script были:
    + меньше кода при создании команд;
    + возможность создавать вложенные команды, это позволяло разбивать их на подклассы. Удобно, когда команд становится более десятка;
    + нормальная документация;
    — пришлось написать небольшую обвязку, чтобы все заработало. Кому-то это может показаться трудной задачей;
    — не является стандартной библиотекой;

    Я бы порекомендовал приглядеться к click. Это отличная замена argparse.
    По опыту использования:
    + очень прост и удобен;
    + минимум кода;
    + куча классных встроенных возможностей: диалоги, группировки и др.
    + хорошая документация;
    — не является стандартной библиотекой;

    Из тяжеловесных инструментов для создания CLI-приложения порекомендовал бы взглянуть на Cement Framework. Сам не пользовался, но отзывы от знакомых были положительные.


    1. Senpos
      18.02.2019 18:20

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

      import click
      
      
      @click.command()
      @click.option(
          "--count",
          default=1,
          type=click.IntRange(min=1, max=100),
          help="Number of files to generate.",
      )
      @click.option(
          "--output",
          type=click.Path(exists=True, file_okay=False, writable=True),
          help="Output directory script will write files to.",
      )
      def file_generator(count, output):
          for i in range(count):
              with open(f"{i}.txt", "w") as f:
                  f.write(f"Dummy data {i}")
      
      
      if __name__ == "__main__":
          file_generator()
      


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

      P. S. Да, во время генерации самих файлов проверки все еще нужны, но это уже другая история. :)

      P. P. S. Форматировался код при помощи black


    1. PyVolshebnyi
      18.02.2019 19:29

      Я так и не разобрался, как в click сделать поддержку списков аргументов, например `--my-args first second third`.

      Можете подсказать?


      1. Senpos
        18.02.2019 20:02

        Судя по документации, можно сделать либо с ограниченным количеством аргументов (в данном случае — 3 аргумента; определит из длины кортежа type):


        import click
        
        @click.command()
        @click.option('--userdata', type=(str, str, int), help='User data represented as "FirstName LastName Age"')
        def greet(userdata):
            print('Hello, user {} {}. You are {} y.o.'.format(*userdata))
        
        if __name__ == '__main__':
            greet()

        Что можно использовать как:
        python app.py --userdata Vasiliy Petrenko 42


        Если нужно передавать несколько одинаковых параметров — можно делать так:


        import click
        
        @click.command()
        @click.option('--name', type=str, help='A name to greet', multiple=True)
        def greet(name):
            for n in name:
                print(f'Hello, {n}')
        
        if __name__ == '__main__':
            greet()

        И использовать как:
        python app.py --name Vasya --name Petya


        Указывать неограниченное количество аргументов для Option (например, python app.py --names Vasya Petya Tolya) — нельзя.


        1. PyVolshebnyi
          18.02.2019 21:41

          Спасибо. Меня это напрягло, учитывая что argparse это умеет.


      1. nvmalovichko
        19.02.2019 00:52

        А без костылей способа, чтобы было как в argparse и красиво, не находил.


        1) В дополнение к вариантам выше:


        import click
        
        @click.command()
        @click.argument('numbers', nargs=-1)
        @click.option('--word')
        def fancy_command(numbers, word):
            result = sum(int(s) for s in numbers)
            print('Numbers sum: {}. Word: {}'.format(result, word))
        
        if __name__ == '__main__':
            fancy_command()

        Получим:


        $ python script.py 1 2 3 4 5 --word Hello
        Numbers sum: 15. Word: Hello

        2) Ну и костыльный способ:


        import click
        
        @click.command()
        @click.option('--numbers', nargs=0)
        @click.argument('numbers', nargs=-1)
        def fancy_command(numbers):
            result = sum(int(s) for s in numbers)
            print('Numbers sum: {}'.format(result))
        
        if __name__ == '__main__':
            fancy_command()

        Получим:


        $ python script.py --numbers 1 2 3
        Numbers sum: 6


  1. PyVolshebnyi
    18.02.2019 19:23

    1) Как-то не раскрыто, что `type` может вам помогать в качестве валидатора, кроме как для приведения значения в тот вид что вам нужен

    import os
    import argparse
    
    def validate_path(path: str) -> str:
        if not os.path.isabs(path):
            raise argparse.ArgumentTypeError(f'Absolute path required, got "{path}"')
    
        return os.path.normpath(path)
    
    parser.add_argument('--path', metavar='PATH', type=validate_path)
    


    2) Если хотите чтобы параметры по умолчанию отображались в помощи, можно использовать такой ArgumentParser

    import argparse
    import typing
    
    
    class RawTextArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
        'Adds default arguments to parser help output'
        def _get_help_string(self, action: argparse.Action) -> str:
            help_str = action.help or ''
            if 'default: ' in help_str or action.default is argparse.SUPPRESS or action.default is None or action.default is False:
                return help_str
    
            if not action.option_strings and action.nargs not in [argparse.OPTIONAL, argparse.ZERO_OR_MORE]:
                return help_str
    
            return f'{help_str} (default: {action.default})'
    
    
    class ArgumentParser(argparse.ArgumentParser):
        '`argparse.ArgumentParser` that shows default values in help message.'
        def __init__(self, **kwargs: typing.Any) -> None:
            kwargs['formatter_class'] = RawTextArgumentDefaultsHelpFormatter
            super(ArgumentParser, self).__init__(**kwargs)
    


  1. keydon2
    18.02.2019 21:57
    -3

    Питон для самых маленьких?
    Документация и читается и пробуется за 30 минут. Зачем нужны такие "гайды"-обрезки — не понятно. К тому же и они уже были:
    https://m.habr.com/ru/post/144416/
    https://jenyay.net/Programming/Argparse
    https://rtfm.co.ua/python-modul-argparse-opcii-komandnoj-stroki-v-primerax/
    Впрочем что я требую от ruvds? У них же никогда не было годных статей...