使用argparse与接受**kwargs参数的函数一起使用

15
我正在使用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()

很明显,当我通过命令行调用location_by_coordinate()函数时,上述main()函数可以正常工作,如下所示:

$ 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传递给函数的内容? 答案:

我该如何启用argparse来处理从命令行输入的意图通过**kwargs传递给函数的内容?


你也可以看一下一个建立在argparse之上的包,叫做plac。它试图基于一个或多个函数的参数定义来填充解析器。https://pypi.python.org/pypi/plac - hpaulj
2个回答

9

你理解了与之相关的情况吗?

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

arguments字典?您定义了一个名为('dest')的“位置”参数,名称为'**kwargs'。您可以将其命名为“foobar”。解析器将字符串“DISTANCE = 3000”分配给args名称空间中的该属性,这在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
这是向argparse添加类似于kwarg的参数的简单方法。
在SO上之前曾经问过如何接受任意字典形式的键值对,例如distance:3000distance=3000。答案总是像我上面草拟的那样解析的某个变化。它可以通过自定义Action类完成,也可以在解析后进行。
哎呀,这个答案几乎是我几天前写的一个复制品: https://dev59.com/TJDea4cB1Zd3GeqPi_Uh#33639147 一个类似的2011年的问题: Using argparse to parse arguments of form "arg=val" 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'])

我有两个位置参数,一个是单个字符串值,另一个是列表(由于+ nargs)。

使用这些args属性调用foo

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']}

我定义了'positionals',但使用'optionals'也同样有效。在命名空间中,位置参数和可选参数之间的区别消失了。

如果我将命名空间转换为字典,我可以通过不同的方式传递值给foo,可以通过*args**kwargs来传递。这完全取决于我如何调用foo,而不是它们在argsarguments中的出现方式。所有这些都不是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'}

感谢您的回答,非常详细。使用argparse创建一个可选参数以使其与接受*args的函数一起使用是否可能?换句话说,可以使用argparse将列表传递给函数吗? - AdjunctProfessorFalcon
1
我不是完全确定你在问什么,但我已经添加了一些关于如何将args值传递给使用*args的函数的示例。 - hpaulj
好的,谢谢,这就是我所问的。非常感谢! - AdjunctProfessorFalcon
1
只是一个提示:您可以设置 parser.add_argument('arg2',nargs='*'),这将使可选参数真正成为可选的。 - Bostone

1
如何启用argparse来处理/解析在命令行输入的内容,以便通过**kwargs传递给函数?
这个命令:
$ python argstest.py location_by_coordinate 40.5949799 -73.9495148 DISTANCE=3000

不执行函数调用:
location_by_coordinate(40.5949799, -73.9495148, DISTANCE=3000)

That is easy to prove:

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

请翻译如下内容:

继续解析参数,你会发现函数并未被调用。因此,你所做的所有关于设置名为 location_by_coordinate 的子解析器的工作都是徒劳的。

argparse 模块只是检查 sys.argv,这是一个简单的字符串列表。每个字符串都是在 python 命令后输入的“单词”之一。

默认情况下,参数字符串取自 sys.argv...
https://docs.python.org/zh-cn/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'。您可以通过以下方式将该字典条目更改为'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.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接