【问题标题】:How ca I get Python ArgParse to stop overwritting positional arguments in child parser如何让 Python ArgParse 停止覆盖子解析器中的位置参数
【发布时间】:2016-09-05 23:19:51
【问题描述】:

我正在尝试让我的脚本正常工作,但 argparse 不断从父解析器覆盖我的位置参数。我怎样才能让 argparse 尊重父母对这些的价值?它确实保留了可选参数中的值。

这是我需要的一个非常简化的版本。如果你运行这个,你会看到 args 被覆盖了。

testargs.py

#! /usr/bin/env python3

import argparse
import sys

def main():
    preparser = argparse.ArgumentParser(add_help=False)
    preparser.add_argument('first',
                        nargs='?')

    preparser.add_argument('outfile',
                        nargs='?',
                        type=argparse.FileType('w', encoding='utf-8'),
                        default=sys.stdout,
                        help='Output file')

    preparser.add_argument(
        '--do-something','-d',
        action='store_true')
    # Parse args with preparser, and find config file
    args, remaining_argv = preparser.parse_known_args()
    print(args)

    parser = argparse.ArgumentParser(
        parents=[preparser],
        description=__doc__)

    parser.add_argument(
        '--clear-screen', '-c',
        action='store_true')
    args = parser.parse_args(args=remaining_argv,namespace=args )
    print(args)

if __name__ == '__main__':
    main()

并使用testargs.py something /tmp/test.txt -d -c 调用它

您会看到它保留了-d,但同时删除了位置参数并将它们恢复为默认值。

编辑:请参阅已接受答案中的其他 cmets 以了解一些注意事项。

【问题讨论】:

  • 为什么不能一次解析所有参数?为什么首先需要预解析器?
  • 只用parser.parse_args() 再次解析原始选项会起作用,不是吗?
  • 重新运行 parser.parse_args() 什么都不做。
  • 我的意思是用简单的args = parser.parse_args() 替换args = parser.parse_args(args=remaining_argv, namespace=args),以便它重新解析已经计算出来的参数。
  • 我必须像这样设置它,因为在真正的脚本中,一些参数必须提前读取,然后我从默认值中提取设置,然后是基于早期参数的配置文件,然后最后一些选项被传入的参数覆盖。我只是试图让这个例子尽可能简单。

标签: python python-3.x argparse


【解决方案1】:

这两个位置是nargs='?'。像这样的位置总是“看到”的,因为空列表匹配 nargs

第一次通过 'text.txt' 匹配 first 并放入命名空间。第二次没有任何字符串匹配,所以使用默认值 - 就像你第一次没有给出那个字符串一样。

如果我将 first 更改为默认的 nargs,我会得到

error: the following arguments are required: first

来自第二个解析器。即使命名空间中有一个值,它仍然会尝试从argv 中获取一个值。 (这就像一个默认值,但不完全是)。

带有nargs='?'(或*)的位置的默认值很棘手。它们是可选的,但与optionals 的方式不同。位置动作仍然被调用,但带有一个空值列表。

我认为parents 功能对您没有任何帮助。 preparser 已经处理了那组参数;无需在 parser 中再次处理它们,尤其是因为所有相关的参数字符串都已被删除。

另一种选择是将父母留在其中,但在第二个解析器中使用默认的sys.argv[1:]。 (但要注意打开文件等副作用)

args = parser.parse_args(namespace=args )

第三种选择是独立解析参数并将它们与字典 update 合并。

 adict = vars(preparse_args)
 adict.update(vars(parser_args))
 # taking some care in who overrides who

更多详情请查看argparse.py 文件ArgumentParser._get_values,特别是not arg_strings 案例。

关于FileType 的注释。该类型适用于您将立即使用文件并退出的小型脚本。对于大型程序,您可能希望在使用后关闭文件(关闭stdout???),或在with 上下文中使用文件,这不太好。


编辑 - 关于parents的注释

add_argument 创建一个Action 对象,并将其添加到解析器的操作列表中。 parse_args 基本上将输入字符串与这些操作相匹配。

parents 只是将那些 Action 对象(通过引用)从父级复制到子级。对于子解析器来说,就好像这些动作是直接用add_argument 创建的一样。

parents 在您导入解析器并且无法直接访问其定义时最有用。如果您同时定义父子节点,那么 parents 只会为您节省一些打字/剪切粘贴。

这个问题和其他 SO 问题(主要触发了按引用复制)表明开发人员并不打算让您同时使用父项和子项进行解析。可以做到,但有一些他们没有考虑到的故障。

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

我可以想象定义一个自定义的Action 类,它会在这种情况下“表现”。例如,它可能会在添加自己的(可能是默认的)值之前检查命名空间是否有一些非默认值。

考虑一下,例如,如果我将 firstaction 更改为“附加”:

preparser.add_argument('first', action='append', nargs='?')

结果是:

1840:~/mypy$ python3 stack37147683.py /tmp/test.txt -d -c
Namespace(do_something=True, first=['/tmp/test.txt'], outfile=<_io.TextIOWrapper name='<stdout>' mode='w' encoding='UTF-8'>)

Namespace(clear_screen=True, do_something=True, first=['/tmp/test.txt', None], outfile=<_io.TextIOWrapper name='<stdout>' mode='w' encoding='UTF-8'>)

来自第一个解析器,first=['/tmp/test.txt'];从第二个开始,first=['/tmp/test.txt', None]

由于append,第一个项目被保留,第二个解析器添加了一个新的默认值。

【讨论】:

  • “我认为parents 功能对您没有任何帮助......相关的参数字符串已被删除。”如果parents 功能没有被正确使用,它只会解析在前一次传递中尚未处理的参数。除非我的答案很离谱,否则我认为完全使用 parents 功能是问题的根源。
  • 无论如何将“first”添加到第二个解析器,OP 都会遇到此问题。
【解决方案2】:

当您指定parents=[preparser] 时,这意味着parserpreparser 的扩展,并将解析与preparser 相关的所有参数,而这些参数从未给出。

假设preparser 只有一个位置参数firstparser 只有一个位置参数second,当您将解析器设为预解析器的子级时,它需要两个 参数:

import argparse

parser1 = argparse.ArgumentParser(add_help=False)
parser1.add_argument("first")

parser2 = argparse.ArgumentParser(parents=[parser1])
parser2.add_argument("second")
args2 = parser2.parse_args(["arg1","arg2"])
assert args2.first == "arg1" and args2.second == "arg2"

但是,仅传递 parser1 剩余的剩余参数将只是 ['second'],这不是 parser2 的正确参数:

parser1 = argparse.ArgumentParser(add_help=False)
parser1.add_argument("first")
args1, remaining_args = parser1.parse_known_args(["arg1","arg2"])

parser2 = argparse.ArgumentParser(parents=[parser1])
parser2.add_argument("second")

>>> args1
Namespace(first='arg1')
>>> remaining_args
['arg2']
>>> parser2.parse_args(remaining_args)
usage: test.py [-h] first second
test.py: error: the following arguments are required: second

要仅处理第一次传递未处理的参数,请不要将其指定为第二个解析器的父项:

parser1 = argparse.ArgumentParser(add_help=False)
parser1.add_argument("first")
args1, remaining_args = parser1.parse_known_args(["arg1","arg2"])

parser2 = argparse.ArgumentParser() #parents=[parser1]) #NO PARENT!
parser2.add_argument("second")
args2 = parser2.parse_args(remaining_args,args1)

assert args2.first == "arg1" and args2.second == "arg2"

【讨论】:

  • 这是正确答案。我删除了父引用,并传入了命名空间。现在它正在按我的预期工作。感谢您的回答!
  • 只是给感兴趣的人一个便条。这样做时,我丢失了第一个解析器的帮助输出,这意味着我的 positinoal args 没有出现在输出中。所以这确实使它成为部分解决方案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-01-10
  • 2018-02-20
  • 2020-11-10
  • 2020-09-03
  • 2012-06-21
  • 2012-04-25
  • 2018-12-31
相关资源
最近更新 更多