Pythonのtype hintingが充実してきていますね。
今回は、typing.Literal
をコマンドライン引数の候補としても扱う方法を説明します。
TL; DR
typing.get_args()
を使う。
- TL; DR
- はじめに
- そのまま使う場合
- argparse.ArgumentParserのchoicesを使う場合
- typing.Literalのプロパティ
- typing.get_args()でtyping.Literalをlistに変換する
- おわりに
はじめに
この記事では、typing.Literal
を使うユースケースとしてホスト名とプロトコルのスキーム名を得ると、URIを生成するというプログラムを考えます。
プロトコルは簡単化のためにHTTPとHTTPS、そしてFTPのみを対応します。
なお、それぞれのプロトコルのスキーム名は、それぞれの名前を小文字にし、末尾に:
をつけることで表現できます。
早速プログラムを作ってみました。
def generate_uri(hostname: str, scheme: Scheme) -> str: uri = scheme + "//" + hostname return uri
この関数の引数のうち、ホスト名hostname
はstr
を、スキーム名scheme
は以下に定義したScheme
をtype hintingとしました。
これにより、スキーム名はScheme
に記載されたものだけが対応していると判断することができます。
import typing Scheme = typing.Literal["http:", "https:", "ftp:"]
そのまま使う場合
この関数をコマンドラインからそのまま使ってみます。
そのためには、コマンドライン引数の設定を行う必要があります。
コマンドライン引数の設定には、argparse.ArgumentParser
を使います。
import argparse import sys if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("hostname", type=str, help="ホスト名") parser.add_argument("scheme", type=Scheme, help="スキーム名") # 注目 args = parser.parse_args() # schemeがScheme型かバリデーションする if scheme not in Scheme: print("Scheme : " + scheme + " is not supported.") sys.exit(1) uri = generate_uri(hostname=args.hostname, scheme=args.scheme) print(uri)
この方法でスキーム名を指定すると、自分でバリデーションを行う必要があります。
parser.add_argument("scheme", type=Scheme, help="スキーム名")
に注目してください。
このコードによって、コマンドライン引数のうち2つ目がスキーム名として受け付けられるよう設定されています。
しかし、type
引数にScheme
を与えたとしてもコマンドラインのスキーム名が本当にScheme
型なのかはわかりません。
なぜなら、Pythonのtype hintingはあくまでもヒントでしかなく、C言語などの静的型付け言語と異なり型指定の強制力がないからです。
そのため、scheme
がScheme
型であるかをバリデーションしなければいけません。
argparse.ArgumentParser
のchoices
を使う場合
argparse.ArgumentParser
にはバリデーションが働く機構が備わっています。
今回のスキーム名のように、いくつかの候補の中から選択する場合は、parser.add_argument()
にchoices
引数で候補を与えることができます。
# 省略 parser.add_argument("scheme", choices=["http:", "https:", "ftp:"], type=Scheme, help="スキーム名") # choicesを追加した # 省略
choices
引数を設定することで、面倒なバリデーションをargparse.ArgumentParser
に任せることができます。
そのため、スキーム名はhttp:
かhttps:
、ftp:
のいずれかであることがargparse.ArgumentParser
によって保証されます。
ただし、parser.add_argument()
のchoices
はlist
しか受け付けないため、typing.Literal
を与えることができません。
先程の例では、対応しているスキーム名をScheme
の定義とparser.add_argument()
のchoices
引数の2箇所に書くことになります。
これは大変手間で、DRY原則に違反した状態となっています。
ja.wikipedia.org
この状態を改善するため、parser.add_argument()
のchoices
引数にScheme
を与えたいですが、これはできません。
# 省略 parser.add_argument("scheme", choices=Scheme, type=Scheme, help="スキーム名") # 省略
なぜなら、parser.add_argument()
のchoices
はlist
しか受け付けず、typing.Literal
であるScheme
を与えることができないためです。
typing.Literal
のプロパティ
typing.Literal
のプロパティを見ると、list
として振る舞えそうなプロパティがほとんどないことがわかります。
# これは別のプログラムです import typing Scheme = typing.Literal["http:", "https:", "ftp:"] if __name__ == "__main__": print(dir(Scheme)) # 実行結果は次のとおり # ['__args__', '__call__', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattr__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__instancecheck__', '__le__', '__lt__', '__module__', '__mro_entries__', '__ne__', '__new__', '__or__', '__origin__', '__parameters__', '__reduce__', '__reduce_ex__', '__repr__', '__ror__', '__setattr__', '__sizeof__', '__slots__', '__str__', '__subclasscheck__', '__subclasshook__', '__weakref__', '_getitem', '_inst', '_name', '_paramspec_tvars', '_typevar_types', 'copy_with']
そのため、list()
を使ってtyping.Literal
をlist
に変換することはできません。
typing.get_args()
でtyping.Literal
をlist
に変換する
typing.Literal
をlist
に変換するには、typing.get_args()
を使います。
この関数は、引数に与えられたエイリアスに含まれる型をタプルとして返します。
例えば、dict[str, int]
を与えると(str, int)
を、typing.Literal["foo", "bar"]
を与えると("foo", "bar")
を得ることができます。
docs.python.org
typing.get_args()
を使うことで、スキーム名のtype hintingをchoices
引数に与えることができます。
# 省略 parser.add_argument("scheme", choices=typing.get_args(Scheme), type=Scheme, help="スキーム名") # 省略
choices
でScheme
を指定することができたことで、DRY原則を保ちながらバリデーションをargparse.ArgumentParser
に任せることができます。
おわりに
この記事では、Pythonでtyping.Literal
に入れたアノテーションをargparse.ArgumentParser
のchoices
に渡す方法を紹介しました。
typing.Literal
はlist
ではないため、list
しか許容しないchoices
に与えることができません。
そこで、typing.get_args()
をアノテーションに使ってtyping.Literal
をlist
へ変換し、choices
へ与えました。
typing.get_args()
を使うことで、コード全体は次のようになります。
import argparse import typing Scheme = typing.Literal["http:", "https:", "ftp:"] def generate_uri(hostname: str, scheme: Scheme) -> str: uri = scheme + "//" + hostname return uri if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("hostname", type=str, help="ホスト名") parser.add_argument("scheme", choices=typing.get_args(Scheme), type=Scheme, help="スキーム名") args = parser.parse_args() uri = generate_uri(hostname=args.hostname, scheme=args.scheme) print(uri)