Pythonに型ヒントが入ってからしばらく経ちます。型ヒントの立ち位置も、なんでもできるアノテーションとして導入されましたが、型ヒント以外の用途はあまり育たず、型ヒントが中心になり、PEPや仕様もそれに合わせて変化したり、より書きやすいように機能が追加されてきました。
本エントリーでは、Python 3.9時点での最新情報を元に、現在サポート中のPythonバージョン(3.6以上)との互換性の情報も織り交ぜながら、最新の型ヒントの書き方を紹介します。
本エントリーの執筆には、Pythonの型の有識者の@moriyoshi、@aodag、@tk0miya にアドバイスをもらいました。ありがとうございます。
環境構築
Visual Studio Code
Visual Studio Codeの場合は、事前にどのインタプリタを利用するかを設定しておきます。その環境にインストールします。venvを使うにはvenvPathを設定します。
{ |
mypyを有効にします。デフォルトの動作だと、型付けをしていない関数内部のチェックがされないので、--check-untyped-defs
をつけたり、より厳しい--strict
をつけたりすると良いでしょう。
{ |
必要な追加パッケージのインストールが実行されますが、3.9特有の機能はまだPyPIにリリースされている安定版の0.790では対応していません。masterブランチにはいくつかの機能が実装済みなので、0.800リリースまでは最新の開発版をインストールしましょう。
git clone https://github.com/python/mypy.git |
PyCharm
PyCharmはPythonインタプリタ(IntelliJ IDEA Ultimateを使っている場合はSDK設定で追加)の設定をすればmypyのインストールは不要です。
変数の型の型付け
変数名の後ろにコロンと型を付与することで型をつけられます。
i_love_python: bool = True |
以前はPythonのコメントの中に記入していましたが、Python 3.5はすでにEOLなので、PyPIで配布されるライブラリ開発者ももう上記の書き方に統一しても問題ないでしょう。
# Python 3.5以前の書き方 |
関数やメソッドの型付け
関数やメソッドは引数リストと返り値の情報を付与できます。返り値がない場合は-> None
(あるいはtyping.NoReturn
)をつけます。
def greeting(name: str) -> str: |
mypyは--check-untyped-defs
オプションが付与されていない場合、-> None
が付いてない関数は型付けされていないとみなしてエラーチェックしません。返り値がない関数でも忘れずにつけるようにしましょう。PyCharmであれば警告は表示されます。
# mypyは返り値宣言されてない関数の中のエラーはチェックしない |
ユーザー定義クラスの型つけ
クラスは乱暴にいってしまえば、変数と関数を固めたものであるので、これまで説明してきた要素でほぼ説明できます。
class UnidentifiedMysteriousAnimal: |
なお、クラス変数を設定するには、 typing.ClassVar
を利用します。
from typing import ClassVar |
あらゆる型を受け付けるAny
型のある言語にはたいてい「あらゆる型のインスタンスを保持できるany型」があります。Pythonにもtyping.Any
があります。もちろん、なるべく静的に型を決めていき、Any
が登場しないに越したことはないのですが、外部からやってくる情報をハンドリングするときなど、どうしても必要な``ことがあります。
from typing import Any |
型よりも厳しく、特定の文字列や数値のリテラルのみを許可する
型ヒントというと、変数に代入できるオブジェクトの型を限定することが想像されますが、「文字列」ではなくて、「特定の文字列」「数値」ではなく、「特定の数値」など、特定のリテラルのみを保持できる制約を与えることができます。これにより、他の言語のenumのようなことが実現できます。
これはPython 3.8以降のみ対応していますが、PyPIのtyping-extensionsパッケージを利用すれば以前のバージョンでも利用できます。
# Python 3.6, 3.7はpip install typing-extensionsが必要 |
ジェネリクス
リストや集合の型付けを行うには、次のように型名[要素の型]
、辞書はdict[キーの型, 値の型]
など、ブラケットで型変数の設定を行います。mypyは現在リリースされている0.790ではこの書き方はできないので、0.800の開発版が必要です。
# Python 3.9以降のみであればこの行は不要 |
Python 3.8以前はこのような書き方はできず、typingパッケージのものを利用していました。Python 3.9ではこの書き方はdeprecatedになります。Python 3.7とPython 3.8はfrom __future__ import annotations
を先頭に記述すれば使えるようになります。2021年12月がEOLのPython 3.6は__future__
を使った回避はできないため、Python 3.6をサポートするのであれば(広く公開するライブラリの場合など)、こちらの書き方が必要です。
from typing import List |
https://www.python.org/dev/peps/pep-0585/#implementation
新と旧で使うべきジェネリック型定義がどこにあるかは上記のPEPにまとまっています。大雑把にいえば、以前はtyping
パッケージがそのすべてを担っていましたが、Python 3.9以降は次のように各パッケージに分散されています。
- 実際にオブジェクトとして使う型そのものであれば、その型定義をそのまま利用
- リスト、タプル、辞書などの
__builtins__
の要素であれば、list
、tuple
、dict
など collections
パッケージの各クラスであれば、collections.deque
など
- リスト、タプル、辞書などの
- iterable、callableなどのPythonのプロトコル関係は
collections.abc
以下 - コンテキスト関係は
contextlib
、正規表現はre
例えば、コールバック用にcallableオブジェクトを関数引数に渡したり、インスタンス変数に保存したいとします。この場合は関数を受け取れる宣言は次のようにcollections.abc.Callable
を使って書きます。型パラメータの最初は引数の型のリスト、後者は返り値です。
import collections |
古い環境はtyping.Callable
を代わりに使います。
from typing import Callable |
コレクションの種類の使い分け
collections.abc
には多数の型があります。今までのコーディングで、これらを細かく区別して利用することは基本的になかったと思いますが、型付けを行うにあたっては、なるべく制約(メソッド)の少ないコレクションを選択する方がポータビリティが上がります。次の図はcollections.abc
および組み込み型のシーケンスの継承関係(実装上の継承ではなく、メソッドの包含で定義した)の図です。左に行くほど、少ないメソッドを持っており、右側に行くほど、メソッドが多くなります。関数の中で使用しているメソッドを見て、なるべくこの図の左側にある型を選んで使うと良いでしょう。
例えば、関数の中で引数のシーケンスに対して、for
でループを回すだけの使い方をするならばcollections.abc.Iterable
を、さらにin
で存在確認をする必要があれば、Collection
を使うと、さまざまなシーケンスのインスタンスを受けて利用できるようになります。ランダムアクセスが必要であればSequence
を使います。値の変更が必要であればMutable
がついた型を利用します。
例えば、引数の型を安易にlist
を指定してしまうと、set
やdict
を渡せなくなります。特に慣れているからといって安易に具象型(リスト、タプル、辞書、集合)を設定しない方が良いです。といっても、これらの具象型を使う方が理解はしやすいと思うので、まずはこの具象型を当てはめてみて、使う演算子やメソッドが少なくても済むことをを確認したら、少しずつ左側の型に寄せていくと良いかもしれません。
タプルと他のシーケンスの違い
タプルは長さ情報まで固定ですので、要素数分、型を指定します。tuple[int, str, float]
など、型を混ぜて指定もできます。一方、リストなどのシーケンスは要素内の全要素が同じ制約になります。1要素だけ設定すればシーケンスの長さによらず、利用できます。
# Python 3.9の例 |
タプルで同じ型の要素を任意長持つインスタンスに適合する型を書くには、ellipsis演算子(...
)を使います。
t: tuple[str, ...] = ("Monty", "Python's", "Flying", "Circus") |
合併型(Union Type) / オプショナル
引数の型は常に1種類だけ、とは限りません。条件によって文字列や数字の両方を受け入れる関数を作りたいこともあります。
Python 3.10では文字列でも数字でもいい、という条件は次のように|
を使って書きます。この記法も、mypy 0.790では対応せず、mypyのmasterブランチ版のインストールが必要です。
def normalize_year(year: int | str) -> int: |
Python 3.7から3.9であれば、from __future__ import annotations
を記述すればこの記法が使えるようになります。
Python 3.6もサポートしたい場合は、旧式のtyping.Union
を利用して書きます。
from typing import Union |
合併型は同じ型のまま受け入れてくれる関数にそのまま渡す以外は、型を分解しなければ利用できません。isinstance()
などで型のチェックを行うと、その条件を見て、ブロック内の型を絞ってくれます。
def normalize_year(year: int | str) -> int: |
合併型の1つの形として他の言語でいうnullable(未初期化がありえる)のような概念を表現するために、typing.Optional
が提供されています。これはNone
との合併型と等価です。
from typing import Optional |
キャスト
合併型やAny
など、型が一意に決まらない変数のうち、状況から、特定の型である確信がある場合、キャストを使って特定の型であると処理系に伝える方法が提供されています。本来なら、ifの条件分岐などを行う方法の方が実際の変数の値を見ての判断になりますが、緊急的な脱出ハッチとして利用可能です。
from typing import cast |
関数のオーバーロード
複数の関数で、入力の型、および返り値の型が異なる関数を作りたいとします。その時に使うのがオーバーロードです。次の例は、画面表示のときの文字数を計算する関数です。入力の型のバリエーションが増える場合は合併型で対応できますし、返り値も合併型で書くことで、雑に対応は可能ですが、入力値の型によって返り値が決定される(例えば、bool
の時はbool
しか返らない)ことを表現するためにはオーバーロードを使う必要があります。
@overload
を付与したスタブ定義を並べて書く。これはmypyなどの型チェック用の情報提供のためだけのもので、Pythonで実際には実行されるときは上書きされて消えるだけなので、実装はellipse演算子(...
)やpass
でよい- 最後に
@overload
がつかない実際の実装を書く。これは入出力はオーバーロードしたすべてを受け入れる必要があるのでAny
を使うか、型を付けないで実装(mypyオプションに--strict
をつけるとこれはエラーになるが)のどちらかで実装
次の関数は、JSON化にあたっての前処理を行う関数です。例えば、JavaScriptは2**53
を超える数値は浮動小数点数になってしまうので、規格上は制約はないが実装的にintは入れられないものが多いのでfloat
変換するようにしています。
# JSONに出力するのに安全な形式に変換 |
まとめ
ここ数年、バージョンアップのたびに大きく改善されてきた型ヒント。今回の3.9でも大きな変化がありました。一部、from __future__
を使わないと3.10未満では使えない機能もありました。3.10でもさらに進展予定です。
ここでは紹介していない、細かい機能もいくつかあります。より詳細な情報はtypingパッケージや、関連PEPを見ると書かれています。
また、mypyのサイトにあるcheatsheetも参考になると思います。