Python argparse 布尔开关:为什么传入 false,结果却仍然为真

10-01 4阅读

先决定用户要传开关,还是传一个值

一个报告脚本默认启用缓存,调用者希望临时关闭,于是输入缓存参数和单词 false,结果缓存仍然开启。问题往往不在缓存逻辑,而在命令行入口:布尔转换函数只判断字符串是否为空,并不理解这个英文单词表示否定。把错误值继续传给业务层,后面的分支写得再正确也无法补救。

设计接口时先选一种表达方式。无值开关用选项本身表示动作,例如启用缓存和禁用缓存;带值选项则要求后面跟一个明确词汇,例如 true 或 false。两种形式都能表达布尔状态,但解析规则不同。帮助文字、自动化调用和测试必须采用同一份约定。

自动生成正反开关,默认值也要写明

Python 三点九起提供 BooleanOptionalAction,为一个长选项自动建立对应的 no 前缀选项。下面把缓存默认值设为真,省略时沿用默认,正选项开启,反选项关闭。它不消费后续的布尔单词,因此写成开关后再接 false 应当报错,而不是尝试猜测调用者的意思。

自动成对开关不等于互斥组:同一次输入先关闭再开启,后出现的值会覆盖前面的值。如果你的接口要求冲突输入直接失败,可以分别注册 store_true 和 store_false,让它们写入同一个目标,再放进互斥组。示例同时验证这两种有意不同的规则。

Python argparse 布尔开关:为什么传入 false,结果却仍然为真

AI生成概念示意图,非真实界面

import argparse
from contextlib import redirect_stderr
from io import StringIO

bad = argparse.ArgumentParser()
bad.add_argument('--cache', type=bool)
print('type=bool:', bad.parse_args(['--cache', 'false']).cache)

flags = argparse.ArgumentParser(prog='flags', allow_abbrev=False)
flags.add_argument('--cache', action=argparse.BooleanOptionalAction,
                   default=True)
for tokens, expected in [([], True), (['--cache'], True),
                         (['--no-cache'], False),
                         (['--no-cache', '--cache'], True)]:
    actual = flags.parse_args(tokens).cache
    assert actual is expected
    print('flags:', tokens, actual)

paired = argparse.ArgumentParser(prog='paired', allow_abbrev=False)
group = paired.add_mutually_exclusive_group()
group.add_argument('--cache', dest='cache', action='store_true')
group.add_argument('--no-cache', dest='cache', action='store_false')
paired.set_defaults(cache=True)
assert paired.parse_args([]).cache is True
assert paired.parse_args(['--no-cache']).cache is False
print('paired disable:', paired.parse_args(['--no-cache']).cache)

def boolean_text(text):
    values = {'true': True, 'false': False}
    try:
        return values[text.casefold()]
    except KeyError:
        raise argparse.ArgumentTypeError('expected true or false')

value_parser = argparse.ArgumentParser(prog='value', allow_abbrev=False)
value_parser.add_argument('--enabled', type=boolean_text, required=True)
assert value_parser.parse_args(['--enabled', 'TRUE']).enabled is True
assert value_parser.parse_args(['--enabled', 'false']).enabled is False
print('explicit value:', value_parser.parse_args(['--enabled', 'false']).enabled)

def expect_error(parser, tokens, label):
    with redirect_stderr(StringIO()):
        try:
            parser.parse_args(tokens)
        except SystemExit as error:
            assert error.code == 2
        else:
            raise AssertionError('invalid input was accepted')
    print(label + ': rejected')

expect_error(flags, ['--cache', 'false'], 'value after flag')
expect_error(paired, ['--cache', '--no-cache'], 'conflicting flags')
expect_error(value_parser, [], 'missing option')
expect_error(value_parser, ['--enabled'], 'missing value')
expect_error(value_parser, ['--enabled', 'maybe'], 'unknown value')

保存为 Python 文件后直接运行,代码只解析内存中的参数列表。开头会显示错误转换把 false 变成 True;自动开关依次得到 True、True、False、True。显式成对选项能正确关闭缓存,带值选项得到 False,最后五项错误输入都应显示 rejected,而不是继续执行。

必填选项和必填值是两个检查

带值解析器使用一个转换函数,只接受大小写不限的 true 与 false,其他文字都抛出参数类型错误。required=True 要求选项必须出现;选项出现以后,默认的取值数量还要求恰好提供一个值。缺少整个选项、只有选项没有值、值不在词汇表内,是三个独立的失败样本。

这里没有自动去掉值两边的空白,也没有把零、一、yes 或 no 当作别名。这是示例的明确契约,不是所有命令行工具的统一标准。若为了兼容已有调用必须接受更多写法,应把它们逐一列入映射并测试,不能把所有未识别字符串都默认为真或默认为假。

把默认、覆盖和报错都纳入验收

例子用重定向暂时收集错误文字,只为让演示输出简短,并断言解析失败的退出码为二。正式脚本通常让解析器正常打印用法和错误;不要照搬这个辅助函数后悄悄吞掉诊断。真正读取终端参数时,调用 parse_args 时不传列表即可,解析器会读取程序收到的参数。

关闭长选项缩写可以减少接口含糊:将来新增一个名称相似的选项,不会让旧的缩写突然指向不同规则。布尔值解析成功后,再验证它与其他参数的业务关系。例如禁用缓存时是否允许填写缓存目录,应单独给出一致的处理方式,不能靠布尔转换顺带完成。

若程序还合并配置文件,可把开关默认值设为 None,保留“调用者未指定”这一状态,再按既定优先级合并。此时应使用与 None 的明确比较,不能用真假判断把明确关闭也视为缺失。本文选择默认开启,是为了让每组输出都容易直接核对。

参考资料

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。