argparse(python)是否支持互斥参数组?

26 投票
4 回答
11066 浏览
提问于 2025-04-16 10:26

如果我有这些参数 '-a', '-b', '-c', '-d',使用 add_mutually_exclusive_group() 这个函数的话,我的程序只能使用其中一个。有没有办法把它们组合起来,让程序只接受 '-a 999 -b 999' 或者 '-c 999 -d 999' 其中一种呢?

编辑:为了更清楚,我加了一个简单的程序:

>>> parser = argparse.ArgumentParser()
>>> group = parser.add_mutually_exclusive_group()
>>> group.add_argument('-a')
>>> group.add_argument('-b')
>>> group.add_argument('-c')
>>> group.add_argument('-d')

这样的话,只能调用 ./app.py -a | ./app.py -b | ./app.py -c | ./app.py -d。有没有可能让 argparse 把这些排除组组合起来,这样就只允许调用 ./app.py -a .. -b .. | ./app.py -c .. -d .. 呢?

4 个回答

4

在@hpaulj的评论中提到的关于argparse的增强请求,已经开放了超过九年,所以我想其他人可能会从我刚发现的解决办法中受益。根据这个评论,我发现可以用以下语法给两个不同的互斥组添加选项:

#!/usr/bin/env python                                                                                                                                                                     
import argparse
import os
import sys

def parse_args():
    parser = argparse.ArgumentParser(
        formatter_class=argparse.ArgumentDefaultsHelpFormatter
    )

    parser.add_argument("-d", "--device", help="Path to UART device", default="./ttyS0")

    mutex_group1 = parser.add_mutually_exclusive_group()
    mutex_group2 = parser.add_mutually_exclusive_group()

    mutex_group1.add_argument(
        "-o",
        "--output-file",
        help="Name of output CSV file",
        default="sensor_data_sent.csv",
    )

    input_file_action = mutex_group1.add_argument(
        "-i", "--input-file", type=argparse.FileType("r"), help="Name of input CSV file"
    )

    # See: https://bugs.python.org/issue10984#msg219660
    mutex_group2._group_actions.append(input_file_action)

    mutex_group2.add_argument(
        "-t",
        "--time",
        type=int,
        help="How long to run, in seconds (-1 = loop forever)",
        default=-1,
    )

    # Add missing ']' to usage message
    usage = parser.format_usage()
    usage = usage.replace('usage: ', '')
    usage = usage.replace(']\n', ']]\n')
    parser.usage = usage

    return parser.parse_args()


if __name__ == "__main__":
    args = parse_args()
    print("Args parsed successfully...")
    sys.exit(0)

这个方法对我来说效果还不错:

$ ./fake_sensor.py -i input.csv -o output.csv                                                                                                                                             
usage: fake_sensor.py [-h] [-d DEVICE] [-o OUTPUT_FILE | [-i INPUT_FILE | -t TIME]]
fake_sensor.py: error: argument -o/--output-file: not allowed with argument -i/--input-file

$ ./fake_sensor.py -i input.csv -t 30         
usage: fake_sensor.py [-h] [-d DEVICE] [-o OUTPUT_FILE | [-i INPUT_FILE | -t TIME]]
fake_sensor.py: error: argument -t/--time: not allowed with argument -i/--input-file

$ ./fake_sensor.py -i input.csv
Args parsed successfully...

$ ./fake_sensor.py -o output.csv
Args parsed successfully...

$ ./fake_sensor.py -o output.csv -t 30
Args parsed successfully...

当然,访问argparse的私有成员是比较脆弱的,所以我可能不会在正式代码中使用这种方法。另外,聪明的读者可能会注意到,使用提示信息有点误导,因为它暗示-o-i可以一起使用,但实际上是不能的!不过,我这个脚本只是用来测试,所以我并不太担心。(要真正修复使用提示信息,我觉得需要花费的时间会比我能分配给这个任务的多得多,但如果你知道有什么聪明的解决办法,请留言告诉我。)

5

我最近也遇到了这个问题。根据我对argparse文档的理解,似乎没有简单的方法可以在argparse中实现这个功能。我考虑过使用parse_known_args,但这很快就变成了要写一个专门版的argparse了;-)

也许应该提交一个错误报告。与此同时,如果你愿意让用户多输入一点内容,可以通过子组来实现(就像git和svn的参数那样),例如:

    subparsers = parser.add_subparsers()
    p_ab = subparsers.add_parser('ab')
    p_ab.add_argument(...)

    p_cd = subparsers.add_parser('cd')
    p_cd.add_argument(...)

虽然不是最理想的,但至少可以让你在argparse中得到一些好处,而不需要太多复杂的黑科技。我最后决定不使用开关,而是直接用子解析器操作和必需的子参数。

7

编辑: 没事了。因为 argparse 这个库在调用 group.add_argument 时做了一个很糟糕的选择,必须创建一个选项。这不是我会选择的设计。如果你非常需要这个功能,可以试试 ConflictsOptionParser:

# exclusivegroups.py
import conflictsparse

parser = conflictsparse.ConflictsOptionParser()
a_opt = parser.add_option('-a')
b_opt = parser.add_option('-b')
c_opt = parser.add_option('-c')
d_opt = parser.add_option('-d')

import itertools
compatible_opts1 = (a_opt, b_opt)
compatible_opts2 = (c_opt, d_opt)
exclusives = itertools.product(compatible_opts1, compatible_opts2)
for exclusive_grp in exclusives:
    parser.register_conflict(exclusive_grp)


opts, args = parser.parse_args()
print "opts: ", opts
print "args: ", args

所以当我们调用它的时候,可以看到我们得到了想要的效果。

$ python exclusivegroups.py -a 1 -b 2
opts:  {'a': '1', 'c': None, 'b': '2', 'd': None}
args:  []
$ python exclusivegroups.py -c 3 -d 2
opts:  {'a': None, 'c': '3', 'b': None, 'd': '2'}
args:  []
$ python exclusivegroups.py -a 1 -b 2 -c 3
Usage: exclusivegroups.py [options]

exclusivegroups.py: error: -b, -c are incompatible options.

不过,警告信息并没有告诉你 '-a''-b''-c' 是不兼容的,但其实可以设计一个更合适的错误信息。下面是之前的错误答案。

旧编辑: [这个编辑是错的,虽然如果 argparse 是这样工作的,那该多好啊?] 我之前的回答其实是错误的,你应该可以通过为每组互斥选项指定一个组来使用 argparse。我们甚至可以用 itertools 来简化这个过程。这样就不需要把所有组合都写出来了:

import itertools
compatible_opts1 = ('-a', '-b')
compatible_opts2 = ('-c', '-d')
exclusives = itertools.product(compatible_opts1, compatible_opts2)
for exclusive_grp in exclusives:
    group = parser.add_mutually_exclusive_group()
    group.add_argument(exclusive_grp[0])
    group.add_argument(exclusive_grp[1])

撰写回答