Использование argparse с функцией, которая принимает аргумент **kwargs

Я использую argparse для получения входных данных и передачи их функции, которая принимает в качестве аргументов две переменные и **kwargs.

Вот моя функция:

import requests
import sys
import argparse


def location_by_coordinate(LAT, LNG, **kwargs):
    if not kwargs:
        coordinate_url = "https://api.instagram.com/v1/locations/search?lat=%s&lng=%s&access_token=%s" % (LAT, LNG, current_token)
        r = requests.get(coordinate_url).text
    else:
        coordinate_url = "https://api.instagram.com/v1/locations/search?lat=%s&lng=%s&access_token=%s" % (LAT, LNG, current_token)
        for key, value in kwargs.iteritems():
            if 'DISTANCE' in kwargs:
                distance = kwargs.get('DISTANCE')
                if distance > 5000:
                    print distance
                    print "max distance is 5000m, value is reassigned to default of 1000m"
                    distance = 1000
                    coordinate_url = "https://api.instagram.com/v1/locations/search?lat=%s&lng=%s&access_token=%s" % (LAT, LNG, current_token)
                    r = requests.get(coordinate_url).text
                else:
                    pass
                    coordinate_url = "https://api.instagram.com/v1/locations/search?lat=%s&lng=%s&access_token=%s" % (LAT, LNG, current_token)
                    r = requests.get(coordinate_url).text
            if 'FACEBOOK_PLACES_ID' in kwargs:
                fb_places_id = kwargs.get('FACEBOOK_PLACES_ID')
                payload = {'FACEBOOK_PLACES_ID': '%s' % (fb_places_id), 'DISTANCE': '%s' % (DISTANCE)}
                r = requests.get(coordinate_url, params=payload).text
            if 'FOURSQUARE_ID' in kwargs:
                foursquare_id = kwargs.get('FOURSQUARE_ID')
                payload = {'FOURSQUARE_ID': '%s' % (foursquare_id), 'DISTANCE': '%s' % (DISTANCE)}
                r = requests.get(coordinate_url, params=payload).text
            if 'FOURSQUARE_V2_ID' in kwargs:
                foursquare_v2_id = kwargs.get('FOURSQUARE_V2_ID')
                payload = {'FOURSQUARE_V2_ID': '%s' % (foursquare_v2_id), 'DISTANCE': '%s' % (DISTANCE)}
                r = requests.get(coordinate_url, params=payload).text
    #print r
    return r

Учитывая эту функцию и использование ею **kwargs, как мне настроить подпарсеры?

Вот как я на данный момент настроил парсер командной строки:

 def main():
        parser = argparse.ArgumentParser(description="API Endpoints tester")
        subparsers = parser.add_subparsers(dest="command", help="Available commands")

        location_by_parser = subparsers.add_parser("location_by_coordinate", help="location function")
        location_by_parser.add_argument("LAT", help="latitude")
        location_by_parser.add_argument("LNG", help="longitude")

        arguments = parser.parse_args(sys.argv[1:])
        arguments = vars(arguments)
        command = arguments.pop("command")
        if command == "location_by_coordinate":
            LAT, LNG = location_by_coordinate(**arguments)
        else:
            print "No command provided..."

    if __name__ == "__main__":
        main()

Очевидно, что указанная выше функция main() отлично работает с функцией location_by_coordinate(), когда я вызываю ее из командной строки следующим образом:

$ python argstest.py location_by_coordinate 40.5949799 -73.9495148

Но с кодом, как сейчас, если я попытаюсь:

$ python argstest.py location_by_coordinate 40.5949799 -73.9495148 DISTANCE=3000

Очевидно, я получаю:

argstest.py: error: unrecognized arguments: DISTANCE=3000

Но я не уверен, как настроить подпарсер для **kwargs. Если я попытаюсь настроить подпарсер следующим образом:

location_by_parser.add_argument("**kwargs", help="**kwargs")

а затем повторите эту команду:

$ python argstest.py location_by_coordinate 40.5949799 -73.9495148 DISTANCE=3000

Это не работает, потому что объект arguments (который является словарем) становится таким:

{'LAT': '40.5949799', 'LNG': '-73.9495148', 'command': 'location_by_coordinate', '**kwargs': 'DISTANCE=3000'}

И этот Traceback возвращается:

Traceback (most recent call last):
  File "argstest.py", line 118, in <module>
    main()
  File "argstest.py", line 108, in main
    foo = location_by_coordinate(**arguments)
  File "argstest.py", line 40, in location_by_coordinate
    return r
UnboundLocalError: local variable 'r' referenced before assignment

Как я могу включить argparse для обработки/анализа того, что вводится в командной строке, которая предназначена для передачи в функцию через **kwargs?


person AdjunctProfessorFalcon    schedule 14.11.2015    source источник
comment
Вы также можете взглянуть на пакет, который строится поверх argparse под названием plac. Он пытается заполнить синтаксический анализатор на основе определений аргументов одной или нескольких функций. pypi.python.org/pypi/plac   -  person hpaulj    schedule 18.11.2015


Ответы (2)


Вы понимаете, что происходит с

{'LAT': '40.5949799', 'LNG': '-73.9495148', 'command': 'location_by_coordinate', '**kwargs': 'DISTANCE=3000'}

arguments словарь? Вы определили «позиционный» аргумент с именем («dest») из «**kwargs». С тем же успехом вы могли бы назвать его «foobar». Синтаксический анализатор присвоил этому атрибуту в пространстве имен args строку 'DISTANCE=3000', которая превратилась в пару ключ словаря: значение в arguments.

Конечно, вы могли бы поискать arguments['**kwargs'] и проанализировать значение самостоятельно:

v = arguments['**kwargs']  # or pop if you prefer
if v is not None:
    k, v = v.split('=')
    arguments[k] = int(v)

Его можно обобщить для обработки нескольких пар (определяемых с помощью `nargs='*').


argparse не обрабатывает аргументы так же, как функции Python, поэтому нет ничего точно аналогичного **kwargs.

Обычный способ принять что-то вроде distance — с «необязательными» или помеченными аргументами.

parser.add_argument('-d','--distance', type=int, help=...)

который примет

python argstest.py location_by_coordinate 40.5949799 -73.9495148 --distance=3000
python argstest.py location_by_coordinate 40.5949799 -73.9495148 --distance 3000
python argstest.py location_by_coordinate 40.5949799 -73.9495148 --d3000
python argstest.py location_by_coordinate 40.5949799 -73.9495148

Он также может быть настроен на использование --DISTANCE или других имен. В последнем случае пространство имен args будет иметь значение по умолчанию для distance. Значение по умолчанию — None.

Это прямой способ добавления kwarg подобных аргументов к argparse.

Принятие произвольного словаря, такого как пары, distance:3000, distance=3000, было задано ранее на SO. Ответы всегда представляли собой некоторую вариацию разбора, который я набросал выше. Это может быть сделано в пользовательском классе Action или после синтаксического анализа, как я предлагаю.

упс, этот ответ почти клон того, что я написал несколько дней назад: https://stackoverflow.com/a/33639147/901925

Аналогичный вопрос 2011 года: Использование argparse для анализа аргументов формы arg= значение

Python argparse dict arg

=================================

(редактировать)

Пример с функцией, которая принимает *args:

In [2]: import argparse
In [3]: def foo(*args, **kwargs):
   ...:     print('args',args)
   ...:     print('kwargs',kwargs)
   ...:     
In [4]: parser=argparse.ArgumentParser()
In [5]: parser.add_argument('arg1')
In [6]: parser.add_argument('arg2',nargs='+')

In [7]: args=parser.parse_args('one two three'.split())
In [8]: args
Out[8]: Namespace(arg1='one', arg2=['two', 'three'])

Итак, у меня есть 2 позиционных аргумента, один с одним строковым значением, другой со списком (из-за + nargs).

Вызовите foo с этими атрибутами args:

In [10]: foo(args.arg1)
args ('one',)
kwargs {}

In [11]: foo(args.arg1, args.arg2)
args ('one', ['two', 'three'])
kwargs {}

In [12]: foo(args.arg1, arg2=args.arg2)
args ('one',)
kwargs {'arg2': ['two', 'three']}

Я определил «позиционные», но это сработало бы так же хорошо и с «необязательными». Различие между позиционными и необязательными параметрами исчезает в пространстве имен.

Если я преобразую пространство имен в словарь, я могу передавать значения в foo разными способами, либо через *args, либо через **kwargs. Все дело в том, как я называю foo, а не в том, как они появляются в args или arguments. Ничто из этого не является уникальным для argparse.

In [13]: arguments = vars(args)
In [14]: arguments
Out[14]: {'arg2': ['two', 'three'], 'arg1': 'one'}

In [15]: foo(arguments['arg2'], arguments['arg1'])
args (['two', 'three'], 'one')
kwargs {}

In [16]: foo(arguments['arg2'], arguments)
args (['two', 'three'], {'arg2': ['two', 'three'], 'arg1': 'one'})
kwargs {}

In [17]: foo(arguments['arg2'], **arguments)
args (['two', 'three'],)
kwargs {'arg2': ['two', 'three'], 'arg1': 'one'}

In [24]: foo(*arguments, **arguments)
args ('arg2', 'arg1')             # *args is the keys of arguments
kwargs {'arg2': ['two', 'three'], 'arg1': 'one'}

In [25]: foo(*arguments.values(), **arguments)
args (['two', 'three'], 'one')    # *args is the values of arguments
kwargs {'arg2': ['two', 'three'], 'arg1': 'one'}
person hpaulj    schedule 14.11.2015
comment
Спасибо за этот ответ, очень информативно. С помощью argparse можно создать необязательный аргумент, который будет работать с функцией, которая принимает *args? Другими словами, передать список с argparse в функцию? - person AdjunctProfessorFalcon; 18.11.2015
comment
Я не совсем понимаю, о чем вы спрашиваете, но я добавил несколько примеров передачи значений args в функцию, использующую *args. - person hpaulj; 18.11.2015
comment
Хорошо, спасибо, это то, что я спрашиваю. Очень признателен! - person AdjunctProfessorFalcon; 18.11.2015
comment
Просто подсказка: вы можете установить parser.add_argument('arg2',nargs='*'), что сделает необязательные аргументы действительно необязательными. - person Bostone; 24.11.2019

Как я могу включить argparse для обработки/анализа того, что вводится в командной строке, которая предназначена для передачи в функцию через **kwargs?

Эта команда:

$ python argstest.py location_by_coordinate 40.5949799 -73.9495148 DISTANCE=3000

НЕ выполняет вызов функции:

location_by_coordinate(40.5949799, -73.9495148, DISTANCE=3000)

Это легко доказать:

def location_by_coordinate(x, y, **kwargs):
    print "I was called!"

Идите вперед и проанализируйте аргументы, и вы увидите, что функция не вызывается. В итоге вся ваша работа по настройке подпарсера с именем location_by_coordinate оказалась напрасной.

Модуль argparse просто проверяет sys.argv, который представляет собой простой список строк. Каждая строка представляет собой одно из «слов», введенных в командную строку после команды python.

По умолчанию строки аргументов берутся из sys.argv,...
https://docs.python.org/3/library/argparse.html#the-parse-args-method

Да, sys.argv — страшное имя, но список строк — это просто список строк. Если вы посмотрите на документы argparse, все примеры делают это:

parser.parse_args('--foo FOO'.split())

Список строк, который вы создаете с помощью split(), ничем не отличается от некоторого списка строк, на который ссылается sys.argv.

Вам нужно вызвать функцию location_by_coordinate() самостоятельно. Для этого вам нужно получить аргументы из командной строки, собрать аргументы, которые должны быть kwargs в словаре, и вызвать вашу функцию следующим образом:

location_by_coordinate(lat, lon, **my_dict)

Если у вас есть эти значения:

lat = 10
lon = 20
my_dict = {'a': 1, 'b': 2}

тогда вызов функции выше будет эквивалентен:

location_by_coordinate(10, 20, a=1, b=2)

Вот пример:

import argparse

def dostuff(x, y, **kwargs):
    print x, y, kwargs

parser = argparse.ArgumentParser()
parser.add_argument("LAT")
parser.add_argument("LON")
parser.add_argument("--distance")
args = parser.parse_args()
my_dict = {}
my_dict["distance"] = args.distance

dostuff(args.LAT, args.LON, **my_dict)

$ python my_prog.py 10 20 --distance 1
10 20 {'distance': '1'}

Вы также можете получить дикт от парсера:

...
...
args = parser.parse_args()
args_dict = vars(args)
print args_dict

--output:--
{'LAT': '10', 'distance': '1', 'LON': '20'}

lat = args_dict.pop('LAT')
lon = args_dict.pop('LON')
print args_dict

--output:--
{'distance': '1'}

location_by_coordinates(lat, lon, **args_dict)

Если вы хотите, чтобы тип пользователя:

DISTANCE=3000

в командной строке, во-первых, я бы не стал заставлять их набирать все заглавные буквы, поэтому давайте поставим цель:

distance=3000

Добавьте в парсер еще один обязательный аргумент:

location_by_parser.add_argument("distance", help="distance")

Затем после того, как вы проанализируете следующее:

$ python argstest.py location_by_coordinate 40.5949799 -73.9495148 distance=3000

ты можешь сделать это:

arguments = parser.parse_args()
args_dict = vars(arguments)

args_dict будет содержать пару ключ/значение 'distance': 'distance=3000'. Вы можете изменить эту запись dict на 'distance': '3000', выполнив следующие действия:

pieces = args_dict['distance'].split('=')

if len(pieces) == 2 and pieces[0] == 'distance':
    args_dict['distance'] = pieces[1]

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

class DistanceAction(argparse.Action):
    def __call__(self, parser, namespace, values, option_string=None):
        #values => The value for the distance command line arg
        pieces = values.split('=')

        if len(pieces) == 2 and pieces[0] in ['distance', 'wave_action']:  #only allow 'distance=' and 'wave_action='
            setattr(namespace, self.dest, pieces[1]) #The dest key specified in the parser gets assigned the value
        else:
            raise argparse.ArgumentTypeError('Usage: distance=3000.  Only distance=, wave_action= allowed.')

Вы можете использовать действие следующим образом:

location_by_parser.add_argument(
    "distance", 
    help="longitude", 
    action=DistanceAction
)

А если хотите пофантазировать, то можете собрать все name=val аргументов, указанных в командной строке, в один словарь с именем, скажем, keyword_args, что позволит вызывать ваш метод так:

args = parser.parse_args()
args_dict = vars(args)
keyword_args = args_dict["keyword_args"]

location_by_coordinates(lat, lon, **keyword_args)

Вот конфигурация парсера:

location_by_parser.add_argument(
    "keyword_args", 
    help="extra args", 
    nargs='*', 
    action=DistanceAction
)

import argparse
import sys

def location_by_coordinates(x, y, **kwargs):
    print x 
    print y
    print kwargs

class DistanceAction(argparse.Action):
    def __call__(self, parser, namespace, values, option_string=None):
        allowed_keywords = ['distance', 'wave_action']
        keyword_dict = {}

        for arg in values:  #values => The args found for keyword_args
            pieces = arg.split('=')

            if len(pieces) == 2 and pieces[0] in allowed_keywords:
                keyword_dict[pieces[0]] = pieces[1]
            else: #raise an error                                                         
                #Create error message:
                msg_inserts = ['{}='] * len(allowed_keywords)
                msg_template = 'Example usage: distance=3000. Only {} allowed.'.format(', '.join(msg_inserts))
                msg = msg_template.format(*allowed_keywords)

                raise argparse.ArgumentTypeError(msg)

        setattr(namespace, self.dest, keyword_dict) #The dest key specified in the
                                                    #parser gets assigned the keyword_dict--in
                                                    #this case it defaults to 'keyword_args'

parser = argparse.ArgumentParser(description="API Endpoints tester")
subparsers = parser.add_subparsers(dest="command", help="Available commands")

location_by_parser = subparsers.add_parser("location_by_coordinate", help="location function")
location_by_parser.add_argument("LAT", help="latitude")
location_by_parser.add_argument("LNG", help="longitude")
location_by_parser.add_argument("keyword_args", help="extra args", nargs='*', action=DistanceAction)

arguments = parser.parse_args()
args_dict = vars(arguments)

print args_dict

lat = args_dict['LAT']
lon = args_dict['LNG']
keyword_args = args_dict['keyword_args']

location_by_coordinates(lat, lon, **keyword_args)

Пример:

$ python prog.py location_by_coordinate 40.5949799 -73.9495148 distance=3000 wave_action=1.4

{'LAT': '40.5949799', 'LNG': '-73.9495148', 'command': 'location_by_coordinate', 'keyword_args': {'distance': '3000', 'wave_action': '1.4'}}

40.5949799
-73.9495148
{'distance': '3000', 'wave_action': '1.4'}

$ python prog.py location_by_coordinate 40.5949799 -73.9495148 x=10
...
...
  File "2.py", line 25, in __call__
    raise argparse.ArgumentTypeError(msg)
argparse.ArgumentTypeError: Example usage: distance=3000. Only distance=, wave_action= allowed.
person 7stud    schedule 14.11.2015