argparseで引数を1つとるオプションをいい感じで扱えるようにしたい

ここ2~3年ほどPythonで小さなコマンドライン・ツールを書く機会が何度かあったのだが、argparseでコマンドライン引数を解析する時、引数を1つだけとるオプションの扱いに難儀している。

ArgumentParser.add_argument()のキーワード引数nargsに1を設定すればよいのだが、解析結果がリストで返される。なので「args.option_foo[0]」みたいにインデックスを指定する必要がある。APIの思想としては、この挙動は理解できる。しかしAPIを使う側としては面倒くさいのだ。

特に面倒なのが、省略可能なオプションでかつ省略時に適当な既定値が無いケースだ。この場合、「args.option_foo」は空のリストである可能性がある(必須オプションや、キーワード引数defaultを使って適当な既定値を用意できるケースでは、リストが空になることはない)。だから「if args.option_foo:」のように空リストか否か確認するコードと、「args.option_foo[0]」で値を参照するコードを書き分ける必要がある。そうしないと空リストなのに「args.option_foo[0]」で値を参照しようとして例外となる。このコンテキストの切り替えが面倒なのである。

大体「複数の値を取りうるオプション(結果として値が0個になる可能性もあるオプション)」の場合はイテレータを使うので、例外が発生することはまず無い。argparseのnargsの仕様からすれば、引数を1つだけとるオプションが例外的なのである。そして私が作るツールでは「引数を1つだけとるオプション」が大多数を占めているので、例外的状況ばかりなのである。

上記挙動を嫌って、少し前まではキーワード引数nargs'?'を指定していた。この方法なら解析結果を「args.option_foo」という書き方で参照できる。しかし難点として、コマンドライン引数にて「引数無しでオプションを指定する」ということが許容されてしまう。本来は「--option-foo value_foo」みたいに指定して欲しいところを「--option-foo」とだけ指定された時に、argparseが許容してしまうのである(というか、それが'?'の時の仕様通りの挙動なのだが)。

もっと、こう、何か「いい感じ」にできないか考えた結果、現時点ではキーワード引数nargsに1を設定した上で、ArgumentParser.parse_args()ArgumentParser.parse_known_args()の戻り値に以下の関数を適用して「いい感じ」に変換することで対応している。

def simplify_options(opts, as_list=[]):
    def ispublicattr(attr_name):
        return attr_name and attr_name[0] != '_'

    def simplify_attr(attr_name):
        attr = getattr(opts, attr_name)

        if not isinstance(attr, list):
            return attr
        elif attr_name in as_list:
            return attr
        elif not attr:
            return None
        elif len(attr) > 1:
            return attr
        else:
            return attr[0]

    attrs = {name: simplify_attr(name) for name in dir(opts) if ispublicattr(name)}
    Options = collections.namedtuple('Options', list(attrs.keys()))
    return Options(**attrs)

関数simplify_options()のキーワード引数as_listに登録された名前以外のリスト型のattributeを展開した上で、namedtuple化して返している。

使い方はこんな感じ。

parsed, rest = parser.parse_known_args()

# '--foo' と '--bar' の引数はリストのままにしておく。
opts = simplify_options(parsed, ['foo', 'bar'])

リストのままにしておきたいものを明示する必要がある点が面倒と言えば面倒だ。しかし実際のところ、コマンドライン・ツールにて「複数の引数を受け取るオプション」を設ける機会は意外と少ない。だから「毎度面倒な手順を踏む」という状況にはならない。

しかし、もっと良い方法は無いだろうか? 私が見落としているだけで、こんな関数を用意しなくとも、標準の機能でうまい具合に扱える気がするのだが……。