使用 Python 模块 argparse 的单个选项的自定义值名称和多个属性
Custom value name and multiple attributes for single option using Python module argparse
我将此作为问答发布在这里,因为我没有在网上找到可用的解决方案,可能还有其他我一直想知道的解决方案,如果我遗漏了某些要点,请随时更新改进。
问题是
- 如何更改
argparse
模块 设置的帮助消息中选项值的显示名称
- 如何才能
argparse
将选项的值拆分为 ArgumentParser.parse_args()
方法返回的对象的多个属性
在 argparse
模块设置的默认帮助消息中,可选参数所需的值使用大写字母的目标属性名称显示。然而,这可能会导致不受欢迎的冗长概要和选项帮助。例如。考虑脚本 a.py
:
#! /usr/bin/env python
import sys
from argparse import ArgumentParser
parser = ArgumentParser()
parser.add_argument('-a')
parser.add_argument('-b','--b_option')
parser.add_argument('-c','--c_option',dest='some_integer')
args = parser.parse_args()
调用此函数的帮助
>>> a.py -h
usage: SAPprob.py [-h] [-a A] [-b B_OPTION] [-c SOME_INTEGER]
optional arguments:
-h, --help show this help message and exit
-a A
-b B_OPTION, --b_option B_OPTION
-c SOME_INTEGER, --c_option SOME_INTEGER
>>>
选项 -b 和 -c 的值不必要地详细,因为它的大部分对最终用户来说没有用,无法知道输入值保存在哪个属性下。
此外,默认情况下 argparse
只允许将选项值保存到 ArgumentParser.parse_args()
方法返回的对象的单个属性中。然而,有时希望能够使用复杂的期权价值,例如逗号分隔的列表,并已分配给多个属性。当然,选项值的解析可以稍后完成,但是在 argparse
框架内完成所有解析以在错误的用户指定选项值时获得一致的错误消息会很整洁。
解决方案是使用 ArgumentParser
和 Action
class 的自定义版本。在 ArgumentParser
class 中,我们覆盖了 parse_args()
方法,以便能够将 None 值设置为未使用的多个属性(问题 2)。在 Action
class 中,我们向 __init__
方法添加两个参数:
attr
:以逗号分隔的要添加值的属性名称字符串,例如attr="a1,a2,a3"
将期望三个值的逗号分隔列表存储在属性 "a1"、"a2" 和 "a3" 下。如果 attr
是
未使用,使用 dest
并包含逗号,这将取代 attr
的使用,例如dest="a1,a2,a3"
等同于指定 attr="a1,a2,a3"
action_type
:将值转换成的类型,例如int,或用于转换的函数名称。这是必要的,因为类型转换是在调用操作处理程序之前执行的,因此不能使用 type
参数。
下面的代码实现了这些自定义 classes 并在最后给出了一些关于它们调用的示例:
#! /usr/bin/env python
import sys
from argparse import ArgumentParser,Action,ArgumentError,ArgumentTypeError,Namespace,SUPPRESS
from gettext import gettext as _
class CustomArgumentParser(ArgumentParser):
"""
custom version of ArgumentParser class that overrides parse_args() method to assign
None values to not set multiple attributes
"""
def __init__(self,**kwargs):
super(CustomArgumentParser,self).__init__(**kwargs)
def parse_args(self, args=None, namespace=None):
""" custom argument parser that handles CustomAction handler """
def init_val_attr(action,namespace):
### init custom attributes to default value
if hasattr(action,'custom_action_attributes'):
na = len(action.custom_action_attributes)
for i in range(na):
val = None
if action.default is not SUPPRESS and action.default[i] is not None:
val = action.default[i]
setattr(namespace,action.custom_action_attributes[i],val)
def del_tmp_attr(action,args):
### remove attributes that were only temporarly used for help pages
if hasattr(action,'del_action_attributes'):
delattr(args,getattr(action,'del_action_attributes'))
if namespace is None:
namespace = Namespace()
### Check for multiple attributes and initiate to None if present
for action in self._actions:
init_val_attr(action,namespace)
### Check if there are subparsers around
if hasattr(action,'_name_parser_map') and isinstance(action._name_parser_map,dict):
for key in action._name_parser_map.keys():
for subaction in action._name_parser_map[key]._actions:
init_val_attr(subaction,namespace)
### parse argument list
args, argv = self.parse_known_args(args, namespace)
if argv:
msg = _('unrecognized arguments: %s')
self.error(msg % ' '.join(argv))
### remove temporary attributes
for action in self._actions:
del_tmp_attr(action,namespace)
### Check if there are subparsers around
if hasattr(action,'_name_parser_map') and isinstance(action._name_parser_map,dict):
for key in action._name_parser_map.keys():
for subaction in action._name_parser_map[key]._actions:
del_tmp_attr(subaction,namespace)
return args
class CustomAction(Action):
"""
Custom version of Action class that adds two new keyword argument to class to allow setting values
of multiple attribute from a single option:
:type attr: string
:param attr: Either list of/tuple of/comma separated string of attributes to assign values to,
e.g. attr="a1,a2,a3" will expect a three-element comma separated string as value
to be split by the commas and stored under attributes a1, a2, and a3. If nargs
argument is set values should instead be separated by commas and if nargs is set
to an integer value this must be equal or greater than number of attributes, or
if args is set to "*" o "+" the number of values must atleast equal to the number
of arguments. If nars is set and number of values are greater than the number of
attributes the last attribute will be a list of the remainng values. If attr is
not used argument dest will have the same functionality.
:type action_type: single type or function or list/tuple of
:param action_type: single/list of/tuple of type(s) to convert values into, e.g. int, or name(s) of
function(s) to use for conversion. If size of list/tuple of default parameters
is shorter than length of attr, list will be padded with last value in input list/
tuple to proper size
Further the syntax of a keyword argument have been extended:
:type default: any compatible with argument action_type
:param default: either a single value or a list/tuple of of values compatible with input argument
action_type. If size of list/tuple of default parameters is shorter than list of
attributes list will be padded with last value in input list/tuple to proper size
"""
def __init__(self, option_strings, dest, nargs=None, **kwargs):
def set_list_arg(self,kwargs,arg,types,default):
if arg in kwargs:
if not isinstance(kwargs[arg],list):
if isinstance(kwargs[arg],tuple):
attr = []
for i in range(len(kwargs[arg])):
if types is not None:
attr.append(types[i](kwargs[arg][i]))
else:
attr.append(kwargs[arg][i])
setattr(self,arg,attr)
else:
setattr(self,arg,[kwargs[arg]])
else:
setattr(self,arg,kwargs[arg])
del(kwargs[arg])
else:
setattr(self,arg,default)
### Check for and handle additional keyword arguments, then remove them from kwargs if present
if 'attr' in kwargs:
if isinstance(kwargs['attr'],list) or isinstance(kwargs['attr'],tuple):
attributes = kwargs['attr']
else:
attributes = kwargs['attr'].split(',')
self.attr = attributes
del(kwargs['attr'])
else:
attributes = dest.split(',')
na = len(attributes)
set_list_arg(self,kwargs,'action_type',None,[str])
self.action_type.extend([self.action_type[-1] for i in range(na-len(self.action_type))])
super(CustomAction, self).__init__(option_strings, dest, nargs=nargs,**kwargs)
set_list_arg(self,kwargs,'default',self.action_type,None)
# check for campatibility of nargs
if isinstance(nargs,int) and nargs < na:
raise ArgumentError(self,"nargs is less than number of attributes (%d)" % (na))
### save info on multiple attributes to use and mark destination as atribute not to use
if dest != attributes[0]:
self.del_action_attributes = dest
self.custom_action_attributes = attributes
### make sure there are as many defaults as attributes
if self.default is None:
self.default = [None]
self.default.extend([self.default[-1] for i in range(na-len(self.default))])
def __call__(self, parser, namespace, values, options):
### Check if to assign to multiple attributes
multi_val = True
if hasattr(self,'attr'):
attributes = self.attr
elif ',' in self.dest:
attributes = self.dest.split(',')
else:
attributes = [self.dest]
multi_val = False
na = len(attributes)
if self.nargs is not None:
values = values
elif na > 1:
values = values.split(',')
else:
values = [values]
try:
nv = len(values)
if na > nv:
raise Exception
for i in range(na-1):
setattr(namespace,attributes[i],self.action_type[i](values[i]))
vals = []
for i in range(na-1,nv):
vals.append(self.action_type[-1](values[i]))
setattr(namespace,attributes[-1],vals)
except:
if na > 1:
if self.nargs is not None:
types = ' '.join([str(self.action_type[i])[1:-1] for i in range(na)])
if multi_val:
raise ArgumentError(self,"value of %s option must be blank separated list of minimum %d items of: %s[ %s ...]" % (options,na,types,str(self.action_type[-1])[1:-1]))
else:
raise ArgumentError(self,"value of %s option must be blank separated list of %d items of: %s" % (options,na,types))
else:
types = ', '.join([str(self.action_type[i])[1:-1] for i in range(na)])
raise ArgumentError(self,"value of %s option must be tuple or list or comma separated string of %d items of: %s" % (options,na,types))
else:
raise ArgumentError(self,"failed to parse value of option %s" % (options))
### Some example invocations
parser = CustomArgumentParser()
parser.add_argument('-a',dest='n',action=CustomAction,type=int)
parser.add_argument('-b','--b_option',dest='m1,m2,m3',action=CustomAction,attr='b1,b2,b3',action_type=int)
parser.add_argument('-c','--c_option',dest='c1,c2,c3',action=CustomAction)
parser.add_argument('-d','--d_option',dest='d1,d2,d3',action=CustomAction,default=("1","2"))
parser.add_argument('-e','--e_option',dest='n,o,p',action=CustomAction,attr=('e1','e2','e3'),action_type=(int,str),default=("1","2"))
parser.add_argument('-f','--f_option',dest='f1,f2,f3',metavar="b,g,h",action=CustomAction,default=("1","2"),nargs=4)
print parser.parse_args(['-f','a','b','c','d'])
您可以使用其他名称和元变量来控制参数调用行。
如果我定义:
parser.add_argument('-f','--foo','--foo_integer',help='foo help')
parser.add_argument('-m','--m_string',metavar='moo',help='foo help')
我得到这些帮助热线:
-f FOO, --foo FOO, --foo_integer FOO
foo help
-m moo, --m_string moo
foo help
帮助中使用了第一个'long'选项标志。 metavar
参数可让您直接指定该字符串。
Explanation for argparse python modul behaviour: Where do the capital placeholders come from? 是沿着这条线的较早的问题,有一个简短的 metavar
答案。
和
How do I avoid the capital placeholders in python's argparse module?
也有 SO 请求显示帮助,例如:
-f,--foo, --foo_integer FOO foo help
这需要对 HelpFormatter
class 进行自定义。但是设置 metavar=''
会让你分道扬镳:
-f,--foo, --foo_integer foo help (add metavar info to help)
见python argparse help message, disable metavar for short options?
至于拆分参数,可以在自定义操作中完成 class。但是我觉得解析之后再做更简单。您仍然可以发出标准化的错误消息 - 使用 parse.error(...)
调用。
In [14]: parser.error('this is a custom error message')
usage: ipython3 [-h] [-a A] [-b B_OPTION] [-c SOME_INTEGER] [-f FOO] [-m moo]
ipython3: error: this is a custom error message
...
nargs=3
允许您接受 3 个参数(选择您的数字)。 Namespace 值将是一个列表,您可以轻松地将其分配给其他变量或属性。像这样的 nargs
负责计算参数。输入必须 space 分隔,就像其他参数一样。
如果您更喜欢使用逗号分隔列表,请注意逗号+space 分隔。您的用户可能必须在整个列表中加上引号。
我将此作为问答发布在这里,因为我没有在网上找到可用的解决方案,可能还有其他我一直想知道的解决方案,如果我遗漏了某些要点,请随时更新改进。
问题是
- 如何更改
argparse
模块 设置的帮助消息中选项值的显示名称
- 如何才能
argparse
将选项的值拆分为ArgumentParser.parse_args()
方法返回的对象的多个属性
在 argparse
模块设置的默认帮助消息中,可选参数所需的值使用大写字母的目标属性名称显示。然而,这可能会导致不受欢迎的冗长概要和选项帮助。例如。考虑脚本 a.py
:
#! /usr/bin/env python
import sys
from argparse import ArgumentParser
parser = ArgumentParser()
parser.add_argument('-a')
parser.add_argument('-b','--b_option')
parser.add_argument('-c','--c_option',dest='some_integer')
args = parser.parse_args()
调用此函数的帮助
>>> a.py -h
usage: SAPprob.py [-h] [-a A] [-b B_OPTION] [-c SOME_INTEGER]
optional arguments:
-h, --help show this help message and exit
-a A
-b B_OPTION, --b_option B_OPTION
-c SOME_INTEGER, --c_option SOME_INTEGER
>>>
选项 -b 和 -c 的值不必要地详细,因为它的大部分对最终用户来说没有用,无法知道输入值保存在哪个属性下。
此外,默认情况下 argparse
只允许将选项值保存到 ArgumentParser.parse_args()
方法返回的对象的单个属性中。然而,有时希望能够使用复杂的期权价值,例如逗号分隔的列表,并已分配给多个属性。当然,选项值的解析可以稍后完成,但是在 argparse
框架内完成所有解析以在错误的用户指定选项值时获得一致的错误消息会很整洁。
解决方案是使用 ArgumentParser
和 Action
class 的自定义版本。在 ArgumentParser
class 中,我们覆盖了 parse_args()
方法,以便能够将 None 值设置为未使用的多个属性(问题 2)。在 Action
class 中,我们向 __init__
方法添加两个参数:
attr
:以逗号分隔的要添加值的属性名称字符串,例如attr="a1,a2,a3"
将期望三个值的逗号分隔列表存储在属性 "a1"、"a2" 和 "a3" 下。如果attr
是 未使用,使用dest
并包含逗号,这将取代attr
的使用,例如dest="a1,a2,a3"
等同于指定attr="a1,a2,a3"
action_type
:将值转换成的类型,例如int,或用于转换的函数名称。这是必要的,因为类型转换是在调用操作处理程序之前执行的,因此不能使用type
参数。
下面的代码实现了这些自定义 classes 并在最后给出了一些关于它们调用的示例:
#! /usr/bin/env python
import sys
from argparse import ArgumentParser,Action,ArgumentError,ArgumentTypeError,Namespace,SUPPRESS
from gettext import gettext as _
class CustomArgumentParser(ArgumentParser):
"""
custom version of ArgumentParser class that overrides parse_args() method to assign
None values to not set multiple attributes
"""
def __init__(self,**kwargs):
super(CustomArgumentParser,self).__init__(**kwargs)
def parse_args(self, args=None, namespace=None):
""" custom argument parser that handles CustomAction handler """
def init_val_attr(action,namespace):
### init custom attributes to default value
if hasattr(action,'custom_action_attributes'):
na = len(action.custom_action_attributes)
for i in range(na):
val = None
if action.default is not SUPPRESS and action.default[i] is not None:
val = action.default[i]
setattr(namespace,action.custom_action_attributes[i],val)
def del_tmp_attr(action,args):
### remove attributes that were only temporarly used for help pages
if hasattr(action,'del_action_attributes'):
delattr(args,getattr(action,'del_action_attributes'))
if namespace is None:
namespace = Namespace()
### Check for multiple attributes and initiate to None if present
for action in self._actions:
init_val_attr(action,namespace)
### Check if there are subparsers around
if hasattr(action,'_name_parser_map') and isinstance(action._name_parser_map,dict):
for key in action._name_parser_map.keys():
for subaction in action._name_parser_map[key]._actions:
init_val_attr(subaction,namespace)
### parse argument list
args, argv = self.parse_known_args(args, namespace)
if argv:
msg = _('unrecognized arguments: %s')
self.error(msg % ' '.join(argv))
### remove temporary attributes
for action in self._actions:
del_tmp_attr(action,namespace)
### Check if there are subparsers around
if hasattr(action,'_name_parser_map') and isinstance(action._name_parser_map,dict):
for key in action._name_parser_map.keys():
for subaction in action._name_parser_map[key]._actions:
del_tmp_attr(subaction,namespace)
return args
class CustomAction(Action):
"""
Custom version of Action class that adds two new keyword argument to class to allow setting values
of multiple attribute from a single option:
:type attr: string
:param attr: Either list of/tuple of/comma separated string of attributes to assign values to,
e.g. attr="a1,a2,a3" will expect a three-element comma separated string as value
to be split by the commas and stored under attributes a1, a2, and a3. If nargs
argument is set values should instead be separated by commas and if nargs is set
to an integer value this must be equal or greater than number of attributes, or
if args is set to "*" o "+" the number of values must atleast equal to the number
of arguments. If nars is set and number of values are greater than the number of
attributes the last attribute will be a list of the remainng values. If attr is
not used argument dest will have the same functionality.
:type action_type: single type or function or list/tuple of
:param action_type: single/list of/tuple of type(s) to convert values into, e.g. int, or name(s) of
function(s) to use for conversion. If size of list/tuple of default parameters
is shorter than length of attr, list will be padded with last value in input list/
tuple to proper size
Further the syntax of a keyword argument have been extended:
:type default: any compatible with argument action_type
:param default: either a single value or a list/tuple of of values compatible with input argument
action_type. If size of list/tuple of default parameters is shorter than list of
attributes list will be padded with last value in input list/tuple to proper size
"""
def __init__(self, option_strings, dest, nargs=None, **kwargs):
def set_list_arg(self,kwargs,arg,types,default):
if arg in kwargs:
if not isinstance(kwargs[arg],list):
if isinstance(kwargs[arg],tuple):
attr = []
for i in range(len(kwargs[arg])):
if types is not None:
attr.append(types[i](kwargs[arg][i]))
else:
attr.append(kwargs[arg][i])
setattr(self,arg,attr)
else:
setattr(self,arg,[kwargs[arg]])
else:
setattr(self,arg,kwargs[arg])
del(kwargs[arg])
else:
setattr(self,arg,default)
### Check for and handle additional keyword arguments, then remove them from kwargs if present
if 'attr' in kwargs:
if isinstance(kwargs['attr'],list) or isinstance(kwargs['attr'],tuple):
attributes = kwargs['attr']
else:
attributes = kwargs['attr'].split(',')
self.attr = attributes
del(kwargs['attr'])
else:
attributes = dest.split(',')
na = len(attributes)
set_list_arg(self,kwargs,'action_type',None,[str])
self.action_type.extend([self.action_type[-1] for i in range(na-len(self.action_type))])
super(CustomAction, self).__init__(option_strings, dest, nargs=nargs,**kwargs)
set_list_arg(self,kwargs,'default',self.action_type,None)
# check for campatibility of nargs
if isinstance(nargs,int) and nargs < na:
raise ArgumentError(self,"nargs is less than number of attributes (%d)" % (na))
### save info on multiple attributes to use and mark destination as atribute not to use
if dest != attributes[0]:
self.del_action_attributes = dest
self.custom_action_attributes = attributes
### make sure there are as many defaults as attributes
if self.default is None:
self.default = [None]
self.default.extend([self.default[-1] for i in range(na-len(self.default))])
def __call__(self, parser, namespace, values, options):
### Check if to assign to multiple attributes
multi_val = True
if hasattr(self,'attr'):
attributes = self.attr
elif ',' in self.dest:
attributes = self.dest.split(',')
else:
attributes = [self.dest]
multi_val = False
na = len(attributes)
if self.nargs is not None:
values = values
elif na > 1:
values = values.split(',')
else:
values = [values]
try:
nv = len(values)
if na > nv:
raise Exception
for i in range(na-1):
setattr(namespace,attributes[i],self.action_type[i](values[i]))
vals = []
for i in range(na-1,nv):
vals.append(self.action_type[-1](values[i]))
setattr(namespace,attributes[-1],vals)
except:
if na > 1:
if self.nargs is not None:
types = ' '.join([str(self.action_type[i])[1:-1] for i in range(na)])
if multi_val:
raise ArgumentError(self,"value of %s option must be blank separated list of minimum %d items of: %s[ %s ...]" % (options,na,types,str(self.action_type[-1])[1:-1]))
else:
raise ArgumentError(self,"value of %s option must be blank separated list of %d items of: %s" % (options,na,types))
else:
types = ', '.join([str(self.action_type[i])[1:-1] for i in range(na)])
raise ArgumentError(self,"value of %s option must be tuple or list or comma separated string of %d items of: %s" % (options,na,types))
else:
raise ArgumentError(self,"failed to parse value of option %s" % (options))
### Some example invocations
parser = CustomArgumentParser()
parser.add_argument('-a',dest='n',action=CustomAction,type=int)
parser.add_argument('-b','--b_option',dest='m1,m2,m3',action=CustomAction,attr='b1,b2,b3',action_type=int)
parser.add_argument('-c','--c_option',dest='c1,c2,c3',action=CustomAction)
parser.add_argument('-d','--d_option',dest='d1,d2,d3',action=CustomAction,default=("1","2"))
parser.add_argument('-e','--e_option',dest='n,o,p',action=CustomAction,attr=('e1','e2','e3'),action_type=(int,str),default=("1","2"))
parser.add_argument('-f','--f_option',dest='f1,f2,f3',metavar="b,g,h",action=CustomAction,default=("1","2"),nargs=4)
print parser.parse_args(['-f','a','b','c','d'])
您可以使用其他名称和元变量来控制参数调用行。
如果我定义:
parser.add_argument('-f','--foo','--foo_integer',help='foo help')
parser.add_argument('-m','--m_string',metavar='moo',help='foo help')
我得到这些帮助热线:
-f FOO, --foo FOO, --foo_integer FOO
foo help
-m moo, --m_string moo
foo help
帮助中使用了第一个'long'选项标志。 metavar
参数可让您直接指定该字符串。
Explanation for argparse python modul behaviour: Where do the capital placeholders come from? 是沿着这条线的较早的问题,有一个简短的 metavar
答案。
和
How do I avoid the capital placeholders in python's argparse module?
也有 SO 请求显示帮助,例如:
-f,--foo, --foo_integer FOO foo help
这需要对 HelpFormatter
class 进行自定义。但是设置 metavar=''
会让你分道扬镳:
-f,--foo, --foo_integer foo help (add metavar info to help)
见python argparse help message, disable metavar for short options?
至于拆分参数,可以在自定义操作中完成 class。但是我觉得解析之后再做更简单。您仍然可以发出标准化的错误消息 - 使用 parse.error(...)
调用。
In [14]: parser.error('this is a custom error message')
usage: ipython3 [-h] [-a A] [-b B_OPTION] [-c SOME_INTEGER] [-f FOO] [-m moo]
ipython3: error: this is a custom error message
...
nargs=3
允许您接受 3 个参数(选择您的数字)。 Namespace 值将是一个列表,您可以轻松地将其分配给其他变量或属性。像这样的 nargs
负责计算参数。输入必须 space 分隔,就像其他参数一样。
如果您更喜欢使用逗号分隔列表,请注意逗号+space 分隔。您的用户可能必须在整个列表中加上引号。