nikkie-ftnextの日記

イベントレポートや読書メモを発信

CLIツールで省略可能な位置引数が複数ある場合、すべてオプション引数に置き換えたい

はじめに

かがみの孤城日本アカデミー賞 優秀アニメーション作品賞 受賞1おめでとうございます!! nikkieです

Pythonargparseを例に、CLIツールにおける省略可能な位置引数について考えていきます。
argparseを使ったコードを書いたことがあることを前提にしています(チュートリアルレベルでかまいません)。

目次

位置引数とオプション引数

※以前argparseについて書いた記事と用語を揃えます

ArgumentParseradd_argumentメソッドには、単一の名前(name)か、複数のフラグ(flags)が渡せます2
ドキュメントの例の抜粋です:

>>> parser.add_argument('-f', '--foo')  # 複数のフラグを渡す例
>>> parser.add_argument('bar')  # 単一の名前を渡す例

parse_args() が呼ばれたとき、オプション引数は接頭辞 - により識別され、それ以外の引数は位置引数として扱われます:

  • 単一の名前👉位置引数
  • 複数のフラグ👉オプション引数

引数の省略

オプション引数の省略

オプション引数は(optionalの名の通り)指定が必須ではありません。
add_argumentメソッドのdefault引数のドキュメント3によると

add_argument() の default キーワード引数 (デフォルト: None) は、コマンドライン引数が存在しなかった場合に利用する値を指定します。

オプション引数では、オプション文字列がコマンドライン上に存在しなかったときに default の値が利用されます:

上の例parser.add_argument('-f', '--foo')だと、-f--fooが指定されないときのデフォルト値はNoneですね。

位置引数の省略

位置引数も省略できます!
引き続きdefault引数のドキュメントによると

nargs が ? か * である位置引数では、コマンドライン引数が指定されなかった場合 default の値が使われます。

>>> parser.add_argument('bar', nargs='?')  # デフォルト値はNone

nargs引数のドキュメント4より

'?' -- 可能なら1つの引数がコマンドラインから取られ、1つのアイテムを作ります。コマンドライン引数が存在しない場合、default の値が生成されます。

引数が省略できる(デフォルト値が使われる)のは、便利ではあります。

複数の位置引数の一部を省略したい

説明用に、以下のようなスクリプトを用意しました(Python 3.10.2で動作確認)。

import argparse

parser = argparse.ArgumentParser()
parser.add_argument("x", nargs="?", default="spam")
parser.add_argument("y", nargs="?")
args = parser.parse_args()

print(vars(args))

位置引数xyはどちらも省略できます。
いくつか呼び出してみましょう。

(1) 位置引数を2つとも渡します。

$ python example.py kokoro fuka
{'x': 'kokoro', 'y': 'fuka'}

先に渡した引数がxに渡りますね。

(2) 位置引数を1つも渡しません。

$ python example.py
{'x': 'spam', 'y': None}

デフォルト値が使われました。

(3) 位置引数を1つだけ渡します。

$ python example.py aki
{'x': 'aki', 'y': None}

前の位置引数xに渡りました。

「前の位置引数はデフォルト値を使い、後ろの位置引数に値を渡したい」ってできる?

位置引数を1つだけ指定して、それを後の位置引数y渡すことはできるのでしょうか?
これはできないと考えています。
いくつか試したり、パーサの気持ちを考えたりした上での結論です。
あくまで"位置"引数なので、渡された実引数の数が仮引数の数より少ないとき、前から当てはめていかざるを得ないように思います5(実装を見れば言い切れそうですね)。

では、「前の位置引数はデフォルト値を使い、後ろの位置引数に値を渡す」が全くできないのかというと、方法はあります。
「2つの位置引数を指定。前の位置引数にはデフォルト値を渡す」で可能です。

$ python example.py spam aki
{'x': 'spam', 'y': 'aki'}

ですが、この方法は引数のデフォルト値を知っている必要があり、使い勝手はあまりよくない印象です。

関数の場合はキーワード引数がオススメされる

コマンドライン引数に限らず、関数の引数でも同様と気付きました。
省略可能な引数=デフォルト値を持った(位置またはキーワード)仮引数です。

デフォルト値を持ったオプションの位置またはキーワード引数には、位置引数でもキーワード引数でも値を渡せます。

def f(a, b, x="spam", y="ham", z="egg"): ...

f(101, 23, "beef")ではxに位置引数で"beef"を渡したことになりyには渡せていません。
yを位置引数で渡すためには、1つ前のxも位置引数で渡す必要があります

つまりf(101, 23, "spam", "beef")ということですね。
これはpython example.py spam akiとしたのと似ていますよね。

この場合yはキーワード引数で渡すとスッキリします。

f(101, 23, y="beef")

xは指定していないのでデフォルト値が使われます。

意見:省略可能な複数の位置引数は、オプション引数に置き換えよう

関数の場合、デフォルト値を持つ仮引数(複数)について、実引数を位置引数からキーワード引数に変更すると書きやすくなりました。
これと同じで、CLIツールでデフォルト値を持つ位置引数を複数実装していたら、すべてオプション引数に変更した方が使いやすくなると考えます。

オプション引数はCLIツールにフラグと一緒に渡します。
これにより

  • 必要な引数だけを渡せばいいので、呼び出しやすい(渡さない引数はデフォルト値が使われる)
  • フラグによって実引数の意味が分かりやすい

というメリットがあると考えます。

終わりに

CLIツールにおいて、デフォルト値をもった複数の位置引数は、オプション引数に置き換えたいということを見てきました。
考え方としては、Pythonの関数で「デフォルト値を持った仮引数は、キーワード専用とする」というtipsに通じるように思います。

argparseを例にしていますが、Clickなど他のCLIツールライブラリでも該当するのではないかと思われます。
例えばClickではargument6(位置引数)をrequired=Falseで指定不要にできます。

P.S. その1 vars(args)

ArgumentParserparse_argsメソッド7はnamespaceオブジェクトを返します。

https://docs.python.org/ja/3/library/argparse.html#the-namespace-object

もし属性を辞書のように扱える方が良ければ、標準的な Python のイディオム vars() を利用できます:

組み込み関数varsを使うと辞書に変換できるので、今回使いました。

なおvarsを使った書き方は、argparse自体のテストコードでも使われています8

class NS(object):
    # 省略
    def __eq__(self, other):
        return vars(self) == vars(other)

P.S. その2 add_argumentconst引数!

https://docs.python.org/ja/3/library/argparse.html#const

nargs引数のドキュメントの例ですが

>>> parser.add_argument('--foo', nargs='?', const='c', default='d')
  • --fooが指定されなければデフォルト値'd'
  • --fooフラグだけ指定されたらconstの'c'
  • --foo barと引数も一緒に指定されたら'bar'

となります。


  1. https://docs.python.org/ja/3/library/argparse.html#name-or-flags
  2. https://docs.python.org/ja/3/library/argparse.html#default
  3. https://docs.python.org/ja/3/library/argparse.html#nargs
  4. デフォルト値を指定した位置引数がxyの2個という単純な例で説明しますが、x,y,zと増やして、引数を1つだけ指定してyに値を渡すようなケースを想像するとパースの難しさが分かるのではないかと思います
  5. https://click.palletsprojects.com/en/8.1.x/arguments/
  6. https://docs.python.org/ja/3/library/argparse.html#the-parse-args-method
  7. https://github.com/python/cpython/blob/v3.10.9/Lib/test/test_argparse.py#L88-L89