Два с половиной приема при работе с argparse


    Приемы, описанные здесь, есть в официальной документации к модулю argparse (я использую Python 2.7), ничего нового я не изобрел, просто, попользовавшись ими некоторое время, убедился в их мощности. Они позволяют улучшить структуру программы и решить следующие задачи:

    1. Вызов определенной функции в ответ на заданный параметр командной строки с лаконичной диспетчеризацией.
    2. Инкапсуляция обработки и валидации введенных пользователем данных.


    Побудительным мотивом к написанию данной заметки стало обсуждение в тостере приблизительно такого вопроса:
    как вызвать определенную функцию в ответ на параметр командной строки
    и ответы на него в духе
    я использую argparse и if/elif
    посмотрите в сторону sys.argv


    В качестве подопытного возьмем сферический скрипт с двумя ветками параметров.
    userdb.py append <username> <age>
    userdb.py show <userid>
    

    Цель, которую позволяет достичь первый прием, будет такой:
    хочу, чтобы для каждой ветки аргументов вызывалась своя функция, которая будет отвечать за обработку всех аргументов ветки, и чтобы эта функция выбиралась автоматически модулем argparse, без всяких if/elif, и еще чтобы… стоп, достаточно пока.

    Рассмотрим первый прием на примере первой ветви аргументов append.
    import argparse
    
    def create_new_user(args):
        """Эта функция будет вызвана для создания пользователя"""
        #скучные проверки корректности данных, с ними разберемся позже
        age = int(args.age)
        User.add(name=args.username, age=args.age)
    
    def parse_args():
        """Настройка argparse"""
        parser = argparse.ArgumentParser(description='User database utility')
        subparsers = parser.add_subparsers()
        parser_append = subparsers.add_parser('append', help='Append a new user to database')
        parser_append.add_argument('username', help='Name of user')
        parser_append.add_argument('age', help='Age of user')
        parser_append.set_defaults(func=create_new_user)
    
        # код для других аргументов
    
        return parser.parse_args()
        
    def main():
        """Это все, что нам потребуется для обработки всех ветвей аргументов"""
        args = parse_args()
        args.func(args)
    

    теперь, если пользователь запустит наш скрипт с параметрами, к примеру:
    userdb.py append RootAdminCool 20

    в недрах программы будет вызвана функция create_new_user(), которая все и сделает. Поскольку у нас для каждой ветви будет сконфигурирована своя функция, точка входа main() получилась по-спартански короткой. Как вы уже заметили, вся хитрость кроется в вызове метода set_defaults(), который и позволяет задать фиксированный параметр с предустановленным значением, в нашем случае во всех ветках должен быть параметр func со значением — вызываемым объектом, принимающим один аргумент.
    Кстати, пользователь, если у него возникнет такое желание, не сможет «снаружи» подсунуть свой параметр в качестве func, не влезая в скрипт (у меня не вышло, по крайней мере).

    Теперь ничего не остается, кроме как рассмотреть второй прием на второй ветке аргументов нашего userdb.py.
    userdb.py show <userid> 


    Цель для второго приема сформулируем так: хочу, чтобы данные, которые передает пользователь, не только валидировались, это слишком просто, но и чтобы моя программа оперировала более комплексными объектами, сформированными на основе данных пользователя. В нашем примере, хочу, чтобы программа, вместо userid получала объект ORM, соответствующий пользователю с заданным ID.

    Обратите внимание, как в первом приеме, в функции create_new_user(), мы делали «нудные проверки» на валидность данных. Сейчас мы научимся переносить их туда, где им самое место.

    В argparse, в помощь нам, есть параметр, который можно задать для каждого аргумента — type. В качестве type может быть задан любой исполняемый объект, возвращающий значение, которое запишется в свойство объекта args. Простейшими примерами использования type могут служить
    parser.add_argument(..., type=file) 
    parser.add_argument(..., type=int)
    

    но мы пройдем по этому пути немного дальше:
    import argparse
    
    def user_from_db(user_id):
        """Возвращает объект-пользователя, если id прошел валидацию, или 
        генерирует исключение.
        """
        # валидируем user_id
        id = int(user_id)
    
        return User.select(id=id)  # создаем объект ORM и передаем его программе
    
    def print_user(args):
        """Отображение информации о пользователе.
        Обращаем внимание на то, что args.userid содержит уже не ID, а объект ORM.
        Этот факт запутывает, но ниже мы с этим разберемся (те самые пол-приема уже близко!)
        """
        user_obj = args.userid
        print str(user_obj)
    
    def parse_args():
        """Настройка argparse"""
        parser = argparse.ArgumentParser(description='User database utility')
    
       # код для других аргументов
    
        subparsers = parser.add_subparsers()
        parser_show = subparsers.add_parser('show', help='Show information about user')
        parser_show.add_argument('userid', type=user_from_db, help='ID of user')
        parser_show.set_defaults(func=print_user)
    
        return parser.parse_args()
    

    Точка входа main() не меняется!

    Теперь, если мы позже поймем, что заставлять пользователя вводить ID как параметр жестоко, мы можем спокойно переключиться на, к примеру, username. Для этого нам потребуется только изменить код user_from_db(), а функция print_user() так ни о чем и не узнает.

    Используя параметр type, стоит обратить внимание на то, что исключения, которые возникают внутри исполняемых объектов, переданных как значения этого параметра, обрабатываются внутри argparse, до пользователя доводится информация об ошибке в соответствующем аргументе.

    Пол-приема.

    Данный трюк не заслужил звания полноценного приема, поскольку является расширением второго, но это не уменьшает его пользы. Если взглянуть на документацию (я про __doc__) к print_user() мы увидим, что на вход подается args.userid, в котором, на самом деле, уже не ID, а более сложный объект с полной информацией о пользователе. Это запутывает код, требует комментария, что некрасиво. Корень зла — несоответствие между информацией, которой оперирует пользователь, и той информацией, которой оперирует наша программа.
    Самое время сформулировать задачу:
    хочу, чтобы названия параметров были понятны пользователю, но, при этом, чтобы код, работающий с этими параметрами был выразительным.
    Для этого у позиционных аргументов в argparse есть параметр metavar, задающий отображаемое пользователю название аргумента (для опциональных аргументов больше подойдет параметр dest).

    Теперь попробуем модифицировать код из второго примера, чтобы решить задачу.
    def print_user(args):
       """Отображение информации о пользователе"""
       print str(args.user_dbobj)
    
    def parse_args():
       """Настройка argparse"""
    
      # код для других аргументов
    
       parser = argparse.ArgumentParser(description='User database utility')
       subparsers = parser.add_subparsers()
       parser_show = subparsers.add_parser('show', help='Show information about user')
       parser_show.add_argument('user_dbobj', type=user_from_db, metavar='userid', help='ID of user')
       parser_show.set_defaults(func=print_user)
    
       return parser.parse_args()
    

    Сейчас пользователь видит свойство userid, а обработчик параметра — user_dbobj.
    По-моему получилось решить обе 2.5 поставленные задачи. В результате код, обрабатывающий данные от пользователя, отделен от основного кода программы, точка входа программы не перегружена ветвлениями, а пользователь и программа работают каждый в своих терминах.

    Рабочий код примера, где уже сразу все “по феншую” находится здесь.
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 18
    • 0
      Направление верное, советую взглянуть на plumbum, в частности его application и subapplications
      • 0
        Спасибо, взял на вооружение. Мощно, но, в моем текущем случае, проигрывает argparse тем, что создает внешнюю зависимость.
      • +2
        Вами проделана отличная работа! Интересно Ваше мнение по поводу этой библиотеки: github.com/docopt/docopt
        • 0
          Остроумно, как раз наткнулся на нее, пока писал статью и смотрел, что еще есть по теме. Но не вижу смысла добавлять внешние зависимости в свои скрипты, пока не столкнулся с реальными ограничениями стандартной библиотеки.
          • 0
            Интересно, оно дружит с локализацией?
          • +1
            Рекомендую не маяться с argparse, а сразу использовать покладистый click.
            • +2
              Я уже выше описал свою мотивацию, да и
              parser = argparse.ArgumentParser()
              parser.add_argument()
              ...
              parser.add_argument()
              

              не такая уж маята по-моему.
              • 0
                1. Описание парсера оторвано от реализации, что значительно усложняет понимание кода. Сопровождать такой код будет тяжело
                2. Реализация многоуровневого вложения команд оставляет желать лучшего

                Для чего-то очень простого и одноразового сойдет. Но если писать вокруг него целый проект, можно поискать решения получше.
            • +3
              Безногая японская школьница… картинка трындец
              • 0
                Отсутствие частей тела не является поводом для пропуска занятий! © у всех был такой препод
                • 0
                  «Без руки и без ноги все равно вперед беги!» (с) мой физрук :)
                • 0
                  Под кат бы эту картинку, а не так. В трекере появляется, немного настроение что ли портит (если мягче выразиться).
                  • 0
                    А что с ней не так? Две с половиной девицы, в центре явный муляж.
                    • +2
                      Вроде не муляж, а ечли и муляж, то физичный. Скорее у нее ноги в шкафчике. Видно что она, в отличии от остальных, на руках сильно висит.
                      • 0
                        Я тоже так сначала подумал, но открытой дверцы на фотографии не видно.
                • +1
                  Я аргументацию про зависимости принял, но не могу не посоветовать (не вам, а читающим) еще и opster. Click конечно больше плюшек имеет, но opster сильно меньше.
                  • 0
                    Мне интересно, а что вы будете делать с argparse, если понадобится man‐страница (а лучше, сразу man page и страница в Sphinx’е)? Я видел pypi.python.org/pypi/sphinx-argparse, но, во‐первых, сгенерированные страницы мне не понравились (usage просто ужас, вёрстка на таблицах, которая в man страницах ещё хуже, чем в HTML), во‐вторых, мне нужно было добавить в создаваемую страницу секцию AUTHORS, причём автоматически созданных, а строки описаний аргументов немного изменить (чтобы не использовать в command --help ReST разметку), а в‐третьих, этот проект использует приватные API.

                    Т.е. в итоге я писал свой велосипед: (сразу говорю, поддерживает только те возможности argparse, что я использую в powerline) github.com/powerline/powerline/blob/6b0cd3d37c1bcf199bf0eb78752a20de9170797e/docs/source/powerline_automan.py. Большинство, кажется, считают, что man страницы не нужны — тот же pip её не имеет (использует optparse).

                    Интересно, а как с этим справляются остальные? В описании ни одной из альтернатив argparse я не увидел ничего про создание документации, отличной от --help. Есть bugs.python.org/issue14102 тоже для argparse, но прилагаемый код опять использует приватные API. Я же подменяю класс ArgumentParser, т.к. никаких публичных API для вытаскивания данных из argparse парсеров просто нет.

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