argparse は Python の標準ライブラリの一つで、主にコマンドライン引数を扱うためのライブラリです。標準であることもあいまって「Python コマンドライン引数」とかでググるとたくさん解説が出てきます(無論、argparse よりも便利であることをアピールするサードパーティライブラリもいくつかありますが)。
一方で、あまり argparse のより踏み込んだ機能について解説している記事を見ない気もします。そこで本記事ではそういった応用的な使い方についていくつか取り上げていこうかと思います。とはいっても普通に公式リファレンスに書いてあることではありますが。
表向きと内部で引数名を変えたい
ユーザー向けの引数名とコード上での名前を別にしたい場合があります。そういう場合にはコード上での名前を ArgumentParser#add_argument
の引数 dest
に渡すことで実現できます。
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--hoge", dest="fuga")
args = parser.parse_args()
# print(args.hoge) # AttributeError
print(args.fuga)
python main.py --hoge foo
foo
この機能は内部的なコードを変更しながらもユーザー向けインタフェースを変更したくない場合などにも有用でしょう。
type(type ではない)
基本的に argparse の ArgumentParser
によってパースされた引数は str
型となりますが、たとえば int
型で取得したいときには ArgumentParser#add_argument
の引数として type=int
を与えると int
型で引数を取得することができます。
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("hoge")
parser.add_argument("fuga", type=int)
args = parser.parse_args()
print(type(args.hoge))
print(type(args.fuga))
python main.py 1 1
<class 'str'>
<class 'int'>
ここで、type
という引数名に気をとられているとわかりづらいのですが、実は type
が要求しているのは型名ではなく str
一つを引数にとる呼び出し可能オブジェクトだったりします。
なので、type
引数はたとえば引数に与えられた文字列を小文字化してから取り込みたい、といった要求にも応えることができます。
import argparse
def to_lower(s: str):
return s.lower()
parser = argparse.ArgumentParser()
parser.add_argument("hoge", type=to_lower)
args = parser.parse_args()
print(args.hoge)
python main.py SAMPLE
sample
ちなみに、引数が存在しなかったときに使用される default
の値に対しては type
に指定したオブジェクトを適用しませんが、default
引数の値が文字列だった場合は別で、type
のオブジェクトが適用されます。この仕様は忘れがちなので注意しましょう。なんでそんな仕様にしたんだ?
nargs を極める
ArgumentParser#add_argument
の nargs
は複数の値を一つの引数に割り当てるための引数です。たとえば一つの引数が二つの値を取り込む場合、以下のように書きます。
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("hoge", nargs=2)
args = parser.parse_args()
print(args.hoge)
python main.py foo bar
['foo', 'bar']
見ての通り、nargs
を設定するとリストで格納されます。これは nargs=1
とした場合でも同様です。
nargs
には整数だけでなく "*"
"+"
といった文字列を指定することもできます。意味は正規表現のそれと同じく「0 を含む任意個数」「0 を含まない任意個数」となります。
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("hoge", nargs="*")
args = parser.parse_args()
print(args.hoge)
python main foo bar baz qux
['foo', 'bar', 'baz', 'qux']
正規表現ライクな文字列として、ほかに "?"
が使用できます。これは省略可能な位置引数を表現するために使えます。この文字列についてはほかの nargs
と異なりリストではなく入力された値そのものが取得され、入力がない場合は default
引数の値(初期値は None
)となる点は注意が必要です。
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("hoge", nargs="?")
args = parser.parse_args()
print(args.hoge)
python main.py foo
foo
python main.py
None
なお、先の節で解説した通り、type
引数はパースされた引数の型を指定するのではなく文字列の処理方法を指定するものです。そのため、nargs
を指定したとしても type
には list
を指定するべきではありません。うっかり指定してしまうと次のようなかなり非直感的なことになります。
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("hoge", nargs="*", type=list)
args = parser.parse_args()
print(args.hoge)
python main.py foo bar baz
[['f', 'o', 'o'], ['b', 'a', 'r'], ['b', 'a', 'z']]
このような結果になるのは、たとえば "foo"
を list
に与えて ["f", "o", "o"]
となり、さらにそれらをリストにまとめたためです。
逆に、複数の整数を受け取りたいときは単に type=int
でよいことになります。
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("hoge", nargs="*", type=int)
args = parser.parse_args()
print(args.hoge)
print(type(args.hoge[0]))
python main.py 1 2 3
[1, 2, 3]
<class 'int'>
action いろいろ
多くの人は "store_true"
くらい (あって "store_false"
) しか使わないと思いますが、action
引数にはほかにも様々なアクションを指定できます。
"store_const"
は指定されると const
に指定した任意の定数を保持します。action="store_true"
はおおまかに action="store_const", const=True, default=False
の略記だと考えることができます。「指定するかしないかの二択にしたいけど bool
型以外で扱いたい」というときには "store_const"
の出番です。
import argparse
parser = argparse.ArgumentParser()
parser.add_argument(
"--use-gpu",
action="store_const",
const="gpu",
default="cpu",
dest="device",
)
args = parser.parse_args()
print(args.device)
python main.py
cpu
python main.py --use-gpu
gpu
action="append"
を指定した引数は指定されるたびにその値をリストに追加します。nargs="*"
との違いは、複数個指定する際その度に名前を書く必要があるということです。どちらの引数仕様が相応わしいかは各アプリケーションによるでしょう。一つ注意点があり、一度も指定されなかった場合は default
に指定した値(未指定なら None
)となるため、通常は default=[]
を明示的に与える必要があります。なんでそんな仕様にしたんだその 2
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--hoge", action="append", default=[])
args = parser.parse_args()
print(args.hoge)
python main.py --hoge foo --hoge bar --hoge baz
['foo', 'bar', 'baz']
python main.py
[]
"append_const"
は "store_const"
と "append"
を組み合わせたような効果を持ちます。以下は引数名によって異なる値を共通したリストに追加していく用法です。
import argparse
parser = argparse.ArgumentParser()
parser.add_argument(
"--hoge",
action="append_const",
const="hoge",
default=[], # これが必要なのやっぱおかしいよ
dest="names",
)
parser.add_argument(
"--fuga",
action="append_const",
const="fuga",
dest="names",
)
parser.add_argument(
"--piyo",
action="append_const",
const="piyo",
dest="names",
)
args = parser.parse_args()
print(args.names)
python main.py --hoge --piyo
['hoge', 'piyo']
"append_const"
を独立した変数に指定して、リストの長さから指定された回数を求めようと思ったあなたはちょっと待ってください。そのような目的には "count"
アクションがぴったりです。注意点として一度も指定されなかった場合は default
に指定した値となるため(数十行ぶり二度目)、通常は default=0
を明示的に与える必要があるということです。なんでそんな仕様にしたんだその 3
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("-v", action="count", default=0)
args = parser.parse_args()
print(args.v)
python main.py -vvv
3
python main.py
0
Namespace 継承のススメ
そもそもパース結果の型はなんなのでしょう? 確かめてみましょう。
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("hoge")
args = parser.parse_args()
print(type(args))
python main.py foo
<class 'argparse.Namespace'>
Namespace
というクラスであることがわかりました。
実はこの Namespace
を継承したクラスを作り、パース結果としてそちらを使わせることができます。その場合、ArgumentParser#parse_args
に namespace
引数としてそのクラスのインスタンスを与えてやれば OK です。やってみましょう。クラス名ではなくインスタンスを与える必要があります。
import argparse
class MyNamespace(argparse.Namespace):
pass
parser = argparse.ArgumentParser()
parser.add_argument("hoge")
args = parser.parse_args(namespace=MyNamespace())
print(type(args))
python main.py foo
<class '__main__.MyNamespace'>
自作クラスを使うメリットはいくつかあります。一つは引数に明示的な型ヒントを与えられることです。やってみましょう。
import argparse
class MyNamespace(argparse.Namespace):
str_value: str
int_value: int
def int_function(x: int) -> None:
pass
parser = argparse.ArgumentParser()
parser.add_argument("str_value")
parser.add_argument("int_value", type=int)
args = parser.parse_args(namespace=MyNamespace())
int_function(args.str_value) # Lint Failed
int_function(args.int_value)
このコードを静的検査にかけると int_function(args.str_value)
で型違反が発生します。これはすべての引数が Any
に推論されるデフォルトの Namespace
では発見できないものです。
Pythonista には静的型に興味がない人も多いでしょうが他にもメリットがあります。たとえば自作のクラスにはメソッドを追加できます。以下は引数をもとに別の値を計算するサンプルです。名字と名前からフルネームを計算しています。
import argparse
class MyNamespace(argparse.Namespace):
def full_name(self):
return f"{self.first_name} {self.family_name}"
parser = argparse.ArgumentParser()
parser.add_argument("first_name")
parser.add_argument("family_name")
args = parser.parse_args(namespace=MyNamespace())
print(args.full_name())
python main.py John Doe
John Doe
引数に関する処理をクラスの中にまとめてしまうのもいいアイデアなのではないでしょうか。以下はパーサーの定義をクラスメソッドとして持たせ、さらにパース後のインスタンスを取得する処理もクラスメソッドにしてしまう例です。
import argparse
from typing import Sequence
class MyNamespace(argparse.Namespace):
@classmethod
def parser(cls):
parser = argparse.ArgumentParser()
parser.add_argument("first_name")
parser.add_argument("family_name")
return parser
@classmethod
def parse(cls, args: Sequence[str] | None = None):
parser = cls.parser()
return parser.parse_args(args, namespace=cls())
def full_name(self):
return f"{self.first_name} {self.family_name}"
args = MyNamespace.parse()
print(args.full_name())
python main.py John Doe
John Doe
パース処理を専用クラスに移譲したことにより、なんらかの追加の操作をまとめてそちらに記述することもできます。たとえばバリデーションを追加してみましょう。たとえば名前と名字はそれぞれ 10 文字以内でなければならない、というバリデーションです。
import argparse
from typing import Sequence
class MyNamespace(argparse.Namespace):
@classmethod
def parser(cls):
parser = argparse.ArgumentParser()
parser.add_argument("first_name")
parser.add_argument("family_name")
return parser
@classmethod
def parse(cls, args: Sequence[str] | None = None):
parser = cls.parser()
result = parser.parse_args(args, namespace=cls())
result.validate()
return result
def validate(self):
if len(self.first_name) > 10 or len(self.family_name) > 10:
raise ValueError
def full_name(self):
if self.first_name is None:
return self.family_name
if self.family_name is None:
return self.first_name
return f"{self.first_name} {self.family_name}"
args = MyNamespace.parse()
print(args.full_name())
python main.py John Doe
John Doe
python main.py Arnold Schwarzenegger
Traceback (most recent call last):
File "C:\Sandbox\argparse-test\main.py", line 32, in <module>
args = MyNamespace.parse()
^^^^^^^^^^^^^^^^^^^
File "C:\Sandbox\argparse-test\main.py", line 17, in parse
result.validate()
File "C:\Sandbox\argparse-test\main.py", line 22, in validate
raise ValueError
ValueError
また、メソッドを持たせることができるということはプロパティを持たせることもできるということです。プロパティはフィールドと同じアクセス文法を持ちますので、引数の仕様変更を引数クラスで吸収するといった応用ができます。
たとえば以下の例を見てください。当初の引数仕様では二つの座標をカンマ区切りで入力することになっていました。
import argparse
from typing import Sequence
class MyNamespace(argparse.Namespace):
coordinate: str
@classmethod
def parser(cls):
parser = argparse.ArgumentParser()
parser.add_argument("coordinate")
return parser
@classmethod
def parse(cls, args: Sequence[str] | None = None):
parser = cls.parser()
result = parser.parse_args(args, namespace=cls())
result.validate()
return result
def validate(self):
comma_index = self.coordinate.find(",")
if comma_index < 0:
raise ValueError
_ = int(self.coordinate[:comma_index])
_ = int(self.coordinate[comma_index + 1 :])
args = MyNamespace.parse()
print(args.coordinate)
python main.py 2,3
2,3
しかし、途中からそれぞれの座標値をスペース区切りで入力させたいということになりました。それぞれの座標値は int
型で x
と y
という名前で参照できるようにもしたい、でもこれまでの coordinate
を使っている箇所を修正したくない、となったとします。そんな要求は見捨てろというツッコミはおいておくとして、以下のように対応できます。
import argparse
from typing import Sequence
class MyNamespace(argparse.Namespace):
x: int
y: int
@classmethod
def parser(cls):
parser = argparse.ArgumentParser()
parser.add_argument("x", type=int)
parser.add_argument("y", type=int)
return parser
@classmethod
def parse(cls, args: Sequence[str] | None = None):
parser = cls.parser()
result = parser.parse_args(args, namespace=cls())
result.validate()
return result
def validate(self):
pass
@property
def coordinate(self):
return f"{self.x},{self.y}"
args = MyNamespace.parse()
print(args.coordinate)
print(args.x)
print(args.y)
python main.py 2 3
2,3
2
3
さらに、やっぱり前の仕様で慣れてる人も多いから戻してくれ、ただスペース区切りでの入力に慣れた人もいるだろうからそれはそのままできるようにして、さらにカンマのあとにスペースが入っててもエラーにならないようにしてほしい、みたいな依頼が来たとします。もうそのクライアント*せというツッコミはおいておくとして、以下のように対応できます。
import argparse
from typing import Sequence
class MyNamespace(argparse.Namespace):
coordinate_a: str
coordinate_b: str | None
@classmethod
def parser(cls):
parser = argparse.ArgumentParser()
parser.add_argument("coordinate_a")
parser.add_argument("coordinate_b", nargs="?")
return parser
@classmethod
def parse(cls, args: Sequence[str] | None = None):
parser = cls.parser()
result = parser.parse_args(args, namespace=cls())
result.validate()
return result
def validate(self):
if self.coordinate_b is None:
comma_index = self.coordinate.find(",")
if comma_index < 0:
raise ValueError
_ = int(self.coordinate_a[:comma_index])
_ = int(self.coordinate_a[comma_index + 1 :])
else:
_ = int(self.coordinate_a.strip(","))
_ = int(self.coordinate_b.strip(","))
pass
@property
def coordinate(self):
if self.coordinate_b is None:
return self.coordinate_a
return f"{self.coordinate_a.strip(",")},{self.coordinate_b.strip(",")}"
@property
def x(self):
comma_index = self.coordinate.find(",")
return int(self.coordinate[:comma_index])
@property
def y(self):
comma_index = self.coordinate.find(",")
return int(self.coordinate[comma_index + 1 :])
args = MyNamespace.parse()
print(args.coordinate)
print(args.x)
print(args.y)
python main.py 2,3
2,3
2
3
python main.py 2 3
2,3
2
3
python main.py 2, 3
2,3
2
3
結び
ひとまずはこんなところかと思います。みなさん良き argparse ライフを!(なにそれ?)