はじめに
本稿を見てくれた方はPythonでも型付け(型推論)機能を使って、なるべくロバストなコードを書こうとしていると思います。その際に表題のような疑問を持ったことはありませんか?
例えば下記のように foo
はint
型であることはわかりますが、*args
の型ってなんだろう?のような。
def hoge(foo: int, *args): ...
本稿はそんな疑問にお答えします。
動作環境
- python 3.11
- PEP646の機能を利用しない場合は、
typing
ライブラリが使える3.5以降のバージョンであればOK
- PEP646の機能を利用しない場合は、
TL;DR
基本的にPEP484の記載通り可変長引数の型は、可変長引数内のすべてに有効な型をつけるべきです。
def hoge(*args: str): ...
この例では、可変長引数である*args
に入る引数はすべてstr
であるということです。
しかし、引数の型がすべて同一でない場合は、Any
を使わざるを得ないです。
ただし、Python 3.11 以降からは可変長引数の型表現力が上がっているので、そちらで対応できたりします。
気になる方は最後まで読んで下さい!
可変長引数とは
本稿の読者はある程度理解していると思いますが、一応触れます。
可変長引数(かへんちょうひきすう、英: variable length arguments、variadic arguments)とはプログラミング言語において、関数(サブルーチンやメソッドを含む)やマクロの引数が固定ではなく任意の個数となっている引数のことである。 可変引数、可変個引数とも呼ばれる。
めちゃくちゃ簡単に言うと、「引数が任意の数取れるやつ」です。
アンパックについて
Pythonだとこのアンパックという概念は大事だったりするのですが、意外と知らない人が多そうなのでこちらも簡単に言及します。
アンパックとは、list
やtuple
型のようなイテラブルなオブジェクトを一つ一つの変数に詰め直す代入手法です。
例えば、Pythonでリストの値をアンパックする場合、以下のように書くことができます。
foo = [1, 2, 3]
a, b, c = foo
print(a) # 1
print(b) # 2
print(c) # 3
ちなみにイテラブルなオブジェクトならできるので、str
型でもできます。
foo = "foo"
a, b, c = foo
print(a) # f
print(b) # o
print(c) # o
このアンパック操作を一括で行い、なにかに渡してしまいたい場合もあると思います。
その場合に、*
を利用します。
foo = [1, 2, 3]
print(foo[0], foo[1], foo[2]) # print関数にリストの中身を地道に渡した場合
print(*foo) # print関数にアンパックして渡した場合
この*
のアンパックを関数の引数で利用することによって、可変長引数を成立させているわけです。
可変長引数の型について
結論はTLDRの通りですが、良くPython入門記事でこういうの文面を見かけませんか?
*args
はtuple
なので、...
たしかに可変長なんだから一見そう見えるし、その理論で行くとこうじゃないの…?
def hoge(foo: int, *args: tuple):
...
もうすでに気がついていると思いますが、アンパックの意味合いを考えるとここをtuple
とするのは良くなさそうです。ここで扱うべきは「アンパックする対象の型」ではなく、「アンパックされた値の型」ですよね。
そう考えるとPEP484の理由もしっくりきますね。
また、「*args
はtuple
」ではなく、「args
がtuple
」です。
*args
は厳密に言うと、*tuple[Any,...]
のような型が理想でしょう。
ただし、PEP646まではそういった表現ができなかったため、アンパックされた値の型を返すわけです。
高度な可変長引数を扱う場合について
ここまでお話した可変長引数の型付けは「PEP 484 – Type Hints」や「引数の型はすべて同一」であることが前提でした。しかし、実際には下記のように可変長引数の型がすべて同一とは限らない場合もあるかもしれません。
def check_type(*args) -> tuple:
a, b = args[:-1], args[-1]
for i in a:
if not isinstance(i, int):
raise TypeError("not int")
if not isinstance(b, str):
raise TypeError("not str")
return args
この関数は可変長引数の型をチェックする関数で、最後の要素がstr
、それ以外はint
型であることを確認します。
つまりargs
はtuple[int,int,int...,str]
のような型を満たす引数でなければなりませんが直近Python3.10まではそれを上手く表現する方法がありませんでした。
これがPython3.11だといくつかの表現で対応することができます。
詳しくは小出しにしていた「PEP 646 – Variadic Generics」を確認してください。
可変型ジェネリクス
TypeVarTuple
によって可変長引数にジェネリクスを適用できるようになりました。
from typing import TypeVarTuple
Ts = TypeVarTuple("Ts")
def check_type(*args: *Ts) -> tuple[*Ts]: # 地味に*argsがtupleでないことが返り値から分かりましたね。
a, b = args[:-1], args[-1]
for i in a:
if not isinstance(i, int):
raise TypeError("not int")
if not isinstance(b, str):
raise TypeError("not str")
return args
ただ、[int, ... , str]
のように型の内容を固定できるわけではないため、この場合はdocstringなどの補足は必須です。
TypeVarTuple
はジェネリクス用途なのでもっと汎用性のある関数を作りたいときに使われるのかなと思います。
タプル型のアンパック
今回のケースだとこれが最適です。理想の可変長引数の型が生成できます。
def check_type(*args: *tuple[*tuple[int, ...], str]) -> tuple[*tuple[int, ...], str]:
a, b = args[:-1], args[-1]
for i in a:
if not isinstance(i, int):
raise TypeError("not int")
if not isinstance(b, str):
raise TypeError("not str")
return args
*tuple[*tuple[int, ...], str]
部分が小難しいので解説すると
-
*args
の型は*tuple
です(タプルをアンパックしているため) -
*tuple[int, ...], str
は*args
のアンパックされた各値の型を表現している -
*tuple[int, ...]
はint型が0個以上あることを表現している
長ったらしくなるので、別途型の命名をして利用した方が良さそうですね。
余談ですが、PEP646でも*tuple[int, ...]
について言及されています。
アンパックド・アンバウンド・タプルの使用は、PEP484の*args: intの動作と同等であり、int型の0個以上の値を受け入れることができる
def foo(*args: *tuple[int, ...]) -> None: ...
# 同等:
def foo(*args: int) -> None: ...
ただし、*tuple[*tuple[int, ...], str]
を *tuple[int, str]
にはできません。(三段論法的な解釈はしないように)
おわりに
3.11の機能も含め色々と紹介しましたが、個人的なスタンスとしては、
def hoge(*args: str): ...
# あるいは3.11以降であれば
def hoge(*args: *tuple[str...]): ...
で済むような簡単な設計をそもそも目指すべきだと思っています。
どうしても型と順序が…、という場合はNamedtuple
などを利用するのも手だと思います。
それでも駄目な場合に3.11の機能をフル活用することになるんじゃないかなと思います。
ただ、型表現度が高くなってきているのは良いですねー。