寺田 学です。9月の
Pythonの型ヒントとは
Pythonは動的型付け言語です。型を指定せずに変数宣言できますし、関数の引数や戻り値に型を宣言する必要はありません。
Python 3.
Pythonの型ヒントは以下のように記述します。
name: str = "氏名" # 変数nameをstr型と宣言
def f(arg: int) -> str: # 引数argはint型で受取り、戻り値はstr型
return str(arg)
型チェック
Python標準ライブラリでは現状型チェックを行う方法がありません。デファクトスタンダードとなっているのは、mypy
というサードパーティー製パッケージです。
利用するには、pip
コマンドでmypy
をインストールします。
$ pip install mypy
これにより、mypy
コマンドが利用できるようになります。
チェックするにはファイル名を引数に以下のように行います。この例はmypyによる指摘事項が無かった場合です。
$ mypy module_name.py Success: no issues found in 1 source file
以下のように指摘がある状況を作り、再度実行してみます。
def f(arg: int) -> str: # 引数argはint型で受取り、戻り値はstr型
return str(arg)
f("10") # int型を受取る関数にstr型を渡す
$ mypy module_name.py module_name.py:5: error: Argument 1 to "f" has incompatible type "str"; expected "int" Found 1 error in 1 file (checked 1 source file)
さらに、VS CodeやPyCharmなどのIDEにmypyを設定して有効にすると、以下のような表示でエラーを指摘してくれます。
Pythonバージョンによる変化
コレクション型
Python 3.
コレクションの変数を宣言する場合、Python 3.typing.
のような大文字始まりのコレクション名の型をインポートして宣言していました。
Python 3.list
のように宣言することができます。現在では、Python 3.List
は非推奨になっています。
また、Python 3.from __
を記述することで、小文字始まりのlist
などで型宣言することが可能です。
Union
Python 3.|
)
先ほど、dict型の値の宣言で用いたtyping.
ですが、Python 3.|
)Union[str, int]
というのは、文字列型または整数型であることを宣言しています。
また、typing.
で宣言していた、None
またはそれ以外の場合においてもパイプ|
)
この機能をPython 3.from __
とすることで利用ができます。
より明確な型ヒントを付ける
ここからは、複雑なデータ構造を辞書やリストで扱う場合の方法について解説します。
支店とスタッフを示す、以下のようなデータ構造を考えます。
company_branches = {
"東京": {
"001": {
"name": "佐藤",
"is_leader": True,
"leader_period": 3,
},
"005": {"name": "田中", "is_leader": False},
},
"福岡": {
"003": {
"name": "伊藤",
"is_leader": True,
"leader_period": 5,
},
"008": {"name": "山本", "is_leader": False},
"011": {"name": "吉田", "is_leader": False},
},
}
このデータに型ヒントを付けると以下のようになります。
company_branches: dict[str, dict[str, dict[str, str | bool | int]]]
ここで、支店名
def get_staff_names(
data: dict[str, dict[str, dict[str, str | bool | int]]], branch_name: str
) -> list[str | bool | int]: # list[str]としたいが辞書の値がstrと特定できない
staff_names = []
for staff in data[branch_name].values():
staff_names.append(staff["name"])
return staff_names
def show_names(staff_names: list[str | bool | int]) -> str:
return ", ".join(staff_names) # リストの要素がstrとは限らないのでエラーとなる
target: str = "東京"
names = get_staff_names(company_branches, target)
print(show_names(names))
ここでは2つの問題があります。
まずは、関数get_
の戻り値です。キーを指定して取得していて、"name"
は暗黙的にstrとなっていることを期待していますが、型ヒントの上では保証できません。
次に、関数show_
の引数です。ここはリストの中にstrが入っていることを期待していますが、同様に型ヒントの上では保証できません。
TypeGuardを使う
ここでの解決策として、Python 3.TypeGuard
を使う方法があります。これは、PEP 647
TypeGuard
を見る前に、if文を使ってNone
をブロックする方法を確認します。
以下は、float
をint
に変換する関数を示しています。この関数の引数にNone
が渡ってくる場合があり、その時は0
を返すという場合の関数です。
def to_int(data: float | None) -> int:
if data is None: # このif文でNoneの場合をブロック
return 0
return int(data) # この段階でdataはfloatである
if data is None:
を通過した場合、Pythonの型チェッカーではNoneが除外されたということがわかります。この方法はisinstance()
を使っても同様になります。
ただ、今回のケースではリストの中の型を確認する必要があり、Python 3.
それでは、先ほどのスタッフの名前を出力する関数を改造します。
型をチェックするための関数is_
を宣言します。この戻り値はbool
型となります。チェックしたい対象の真偽を返します。この時、戻り値の型をTypeGuard
とし、そのデータの中がどのような型かを宣言します。
from typing import Any, Iterable
from typing import TypeGuard
def is_all_str(strings: Iterable[Any]) -> TypeGuard[Iterable[str]]:
"""引数で与えられたイテラブルの要素の全てが文字列型かを調べる"""
if all(isinstance(s, str) for s in strings):
return True
return False
ここでは、TypeGuard[Iterable[str]]
としましたので、このチェック関数を通過したデータは、Iterable[str]
であることを保証すると宣言しています。
次に関数show_
を改造していきます。
from typing import NoReturn
def show_names(staff_names: list[str | bool | int]) -> str | NoReturn:
"""staffのnameをカンマ区切りで出力"""
if not is_all_str(staff_names):
raise ValueError("str以外のオブジェクトを発見")
return ", ".join(staff_names)
この関数では、型が合わない場合には、ValueError
を返すようにしています。例外の発生によりこの関数の戻り値がなくなりましたので、NoReturn
の場合があることも合わせて宣言しています。
TypeGuard
を使うと、リストなどの要素内の型チェックができるようになります。
TypeAliasでより明確に型ヒントを定義
先ほどの、支店とスタッフを示す辞書に明確な型ヒントを定義します。
from typing import Literal
from typing import TypeAlias
BranchNames = Literal["東京", "福岡"] # Literalを使って定義できる文字列を制限
Name = str # 意味のある名前を付ける
IsLeader = bool
LeaderPeriod = int
Staff = dict[str, Name | IsLeader | LeaderPeriod]
Branch = dict[str, Staff]
CompanyBranches = dict[str, Branch]
ここで注目すべきは2つです。
1つ目はLiteral
を使った文字列定義です。今回の場合、支店が2つと限定されている場合を想定しています。よって、ここでは東京, 福岡
と2つのみが支店名として有効であるということを示しています。
2つ目は、Name
, IsLeader
, LeaderPeriod
という、スタッフの属性を表す3つの要素について、型に名前を付けてわかりやすく表現しています。
このように、グローバル変数に代入した場合、型エイリアスとなります。
これらの宣言により、スタッフを表す辞書Staff
の値は3つの型エイリアスで宣言することができます。さらには、支店のを表すBranch
の値にはStaff
と明確にし、データ構造の辞書自体であるCompanyBranches
がどのようなデータ構造であるかが明確になりました。
この方法は、
URL:https://
ここからは、Python 3.TypeAlias
でより明確にする方法を説明します。
具体的には以下のようになります。
from typing import TypeAlias
Name: TypeAlias = str # TypeAliasとして明確にする
IsLeader: TypeAlias = bool
LeaderPeriod: TypeAlias = int
先ほどの例では、Name = str
とグローバル変数として定義していましたが、これが型エイリアスなのかどうかは使う側が決めるということになります。Name: TypeAlias = str
とすることで、明確に型エイリアスであることを示すことができます。
これは、文字列で宣言できる前方参照の時に有効な手段となります。
MyType: TypeAlias = "ClassName"
def foo() -> MyType: pass
class ClassName: pass
型は文字列でdummy: "str" = "1"
のように宣言することが可能で、クラス定義の前に型ヒントを書くことができます。ただ、型エイリアスの代入では文字列を渡すことができませんでした。
TypeAlias
を用いることで、上記のように型エイリアスを宣言することができるようになります。グローバル変数宣言時には、より分かりやすく明確に書くことができます。
TypedDictの活用
TypedDict
は、キーを固定した辞書TypedDict
を継承したクラスに対して、クラス属性を宣言することで辞書のキーと値のデータ型を明確にしていきます。
ここまで使っている、支店とスタッフの辞書をTypedDictで宣言したいとおもいます。
しかし、ここまで使ってきた辞書は、キー自体に暗黙的に意味を持たせたデータ構造になっています。たとえば
まずは、辞書を変更し、キーに意味を持たせる構造に変更していきます。
定義済みの辞書を再掲載します。
company_branches = {
"東京": {
"001": {
"name": "佐藤",
"is_leader": True,
"leader_period": 3,
},
"005": {"name": "田中", "is_leader": False},
},
"福岡": {
"003": {
"name": "伊藤",
"is_leader": True,
"leader_period": 5,
},
"008": {"name": "山本", "is_leader": False},
"011": {"name": "吉田", "is_leader": False},
},
}
変更後の辞書は以下のようになります。
company_branches = [
{
"branch_name": "東京",
"staff": [
{
"number": "001",
"name": "佐藤",
"is_leader": True,
"leader_period": 3,
},
{"number": "005", "name": "田中", "is_leader": False},
],
},
{
"branch_name": "福岡",
"staff": [
{
"number": "003",
"name": "伊藤",
"is_leader": True,
"leader_period": 5,
},
{"number": "008", "name": "山本", "is_leader": False},
{"number": "011", "name": "吉田", "is_leader": False},
],
},
]
ここでは、データ全体をリストとして、要素を辞書で表すようにしています。いままでは、支店名が辞書のキーでしたが、branch_
キーの値で示すようにしています。さらに、スタッフ番号も同様に変更しています。
参考までに、変更されたデータに対して型定義を掲載しておきます。
BranchNames = Literal["東京", "福岡"]
Number: TypeAlias = str
Name: TypeAlias = str
LeaderPeriod: TypeAlias = int
IsLeader: TypeAlias = bool
Staff = dict[str, Number | Name | IsLeader | LeaderPeriod]
Branch = dict[str, str | list[Staff]]
CompanyBranches = list[Branch]
具体的なTypedDictの使い方
データ構造が変わり、TypedDict
で明確なデータ構造を示せるようになったので、クラスを宣言し型ヒントを宣言します。
from typing import Literal
from typing import TypedDict
class Staff(TypedDict): # TypedDictの継承し、スタッフを表すクラスを宣言
number: str
name: str
is_leader: bool
class LeaderStaff(Staff, total=False): # スタッフを表す辞書にオプショナルキーがあるので継承して別クラスを宣言
leader_period: int
class Branch(TypedDict):
branch_name: str
staff: list[LeaderStaff] # leader_periodが存在する可能性があるので継承されたスタッフ
BranchNames = Literal["東京", "福岡"]
CompanyBranches = list[Branch]
クラスStaff
には、3つの必須キーと、1つのオプションキーがあります。そのため、2つのクラスを作っています。最初に宣言しているStaff
を見てみると、クラス属性に辞書のキーを宣言し、値となるデータ型を宣言します。クラスLeaderStaff
には、total=False
としています。これはここで宣言されたクラス属性は必須ではなく、オプションのキーとなります。
支店を表す構造をクラスBranch
としています。ここには2つのクラス属性を宣言しています。staff
には、リストでスタッフを持つようにしています。リストにはleader_
があってもなくてもいいようにLeaderStaff
としています。
このように辞書に対して明確に型ヒントを付けるには、データ構造から見直す必要があります。ただ、より明確に型ヒントを付けることで、以前の構造ではできなかった、キーを取得した時点でデータ型は定まるという恩恵を受けることができます。
変更した構造に合わせた関数get_
を見ていきましょう。
def get_staff_names(data: CompanyBranches, branch_name: BranchNames) -> list[str]:
"""company_branchesから、branch_nameに所属するstaffのnameをリストで出力"""
staff_names = []
for branch in data:
if branch["branch_name"] == branch_name:
for staff in branch["staff"]:
staff_names.append(staff["name"])
return staff_names
データ構造が変わりましたので、少しコードを変更しています。
この関数の戻り値のデータ型をlist[str]
とすることができるようになりました。これにより、使う側でTypeGuard
を使った判定を行う必要がなくなります。
さらに、TypedDict
では、必須の辞書のキーを決めていますので、辞書を宣言する際のキー設定忘れを型チェックで確認することが可能になります。
オプションのキーをより明確に宣言
2022年10月にリリース予定のPython 3.NotRequired
という新たな仕組みが入ります。これは、PEP 655 (Marking individual TypedDict items as required or potentially-missing)
先ほどのTypedDictの宣言を変更すると以下のようになります。
from typing import TypedDict, NotRequired
class Staff(TypedDict):
number: str
name: str
is_leader: bool
leader_period: NotRequired[int]
class Branch(TypedDict):
branch_name: BranchNames
staff: list[Staff]
1つのTypedDictを継承したクラスの中に、オプションのキーを宣言することができるようになります。
なお、Python 3.typing_
を導入し、以下のようにインポートすると利用できるようになります。
from typing_extensions import TypedDict # TypedDictもtyping_extensionsからインポートする
from typing_extensions import NotRequired
JSONを取り込む
ここまでは、Pythonの辞書やリストを直接定義してきました。実際にはこのようなデータはJSONで渡ってくることが多かと思います。その場合はJSONをPythonのオブジェクトに変換し、型ヒントを宣言することができます。
ここで注意があります。TypedDict
は型ヒントとしてしか機能しません。これはJSONがどのようなものかをチェックする機構が無いということです。JSONで渡ってくるデータ構造は、別の仕組みでチェックをする必要があります。これは、Pythonのサードパーティー製ライブラリである、jsonschemaのようなものでJSONを受け取る段階でチェックを行う必要があるということです。
他の方法として、dataclass
を用いて独自のオブジェクトを作り、より厳密で型安全なコードを書くことができます。その際には、dataclass
の__
メソッドを用いて受け取ったデータが正しいかをチェックしてオブジェクト化する方法もあります。
まとめ
今回は、
Pythonは年に1度の機能アップを伴うマイナーリリースが行われています。リリースごとに型ヒントの機能もアップしており、より型安全なコードが書けるようになっています。
みなさんも徐々に型ヒントを明確に付けるということに挑戦をしていただければと思います。