我如何让 Python ArgParse 停止覆盖 child 解析器中的位置参数

How ca I get Python ArgParse to stop overwritting positional arguments in child parser

我试图让我的脚本正常工作,但 argparse 一直在覆盖我来自 parent 解析器的位置参数。我怎样才能让 argparse 尊重 parent 对这些的价值?它确实保留了可选参数的值。

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

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 但删除了两个位置参数并将它们恢复为默认值。

编辑:请参阅已接受答案中的附加评论以了解一些警告。

2 个位置是 nargs='?'。像这样的位置总是 'seen',因为空列表匹配 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

有关详细信息,请查看位于 ArgumentParser._get_valuesargparse.py 文件,特别是 not arg_strings 个案例。

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


编辑 - 关于 parents

的注释

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

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

parents 在导入解析器且无法直接访问其定义时最有用。如果您同时定义父项和子项,那么 parents 只会为您节省一些 typing/cut-n-paste.

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

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

我可以想象定义一个自定义 Action class 会 'behave' 在这种情况下。例如,它可能会在添加自己的(可能是默认的)值之前检查名称空间中的一些非默认值。

考虑一下,例如,如果我将 firstaction 更改为 'append':

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=[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"