...

воскресенье, 8 марта 2015 г.

Два с половиной приема при работе с 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('userid', type=user_from_db, dest='user_dbobj', help='ID of user')
parser_show.set_defaults(func=print_user)

return parser.parse_args()




Сейчас пользователь видит свойство userid, а обработчик параметра — user_dbobj.

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

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


This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий