やりたいこと
Pythonで「一部のキーは必ずあって、一部のキーはないかもしれない辞書」をtypingしたい。
TypeScriptで書くなら以下のような型を、Pythonで表現したい
interface IMember {
lastName: string
firstName: string
middleName?: string // optional
age?: number // optional
}
TL;DR
コード
from typing import TypedDict
class IMemberRequired(TypedDict):
"""
Required keys
"""
last_name: str
first_name: str
class IMember(IMemberRequired, total=False):
"""
Optional keys
"""
middle_name: str
age: int
member: IMember = get_member()
print(member["last_name"]) # => アラートなし
print(member["middle_name"]) # => アラートあり
エディタ上での表記
※ Pylance拡張機能をインストールしたVSCodeを使っています
説明
TypedDictについて
Pythonで辞書を型定義するときには、まずdict[x,y]
が検討されます。
※ python3.9からリストや辞書の型定義には、typingからimportされるList
やDict
ではなく、組み込みのlist
やdict
を使うことが推奨されています(参照)
my_dict: dict[str,int] = get_dict()
my_age = my_dict["age"] # my_ageはintでtypingされる
ただdict
だと、
- my_dictはこういうkey一覧をもっている
- valueは単一の型じゃない(int以外にもある)
ということが表現できません。
TypedDictを使うと、より柔軟に辞書の型を表現することができます。
from typing import TypedDict
class IMyDict(TypedDict):
age: int
name: str
has_PC: bool
my_dict: IMyDict = get_dict()
my_age = my_dict["hogefuga"] # 存在しないkeyへのアクセス
# Could not access item in TypedDict "hogefuga" is not a defined key in "IMyDict"
my_dict.update({"hoge":111}) # 存在しないkeyを持つ辞書でのupdate
# Argument of type "dict[str, int]" cannot be assigned to parameter "__m" of type "IMyDict" in function "update" "hoge" is an undefined field in type "IMyDict"
加えて適切な拡張機能をエディタにインストールしていれば、keyの自動補完もしてくれます(筆者はVSCodeにPylanceを導入しています)
Optional (Not required) なkeyの表現
「一部のキーは必ずあって、一部のキーはないかもしれない辞書」を表現したいことがあります。「クライアントからJSONでリクエストが渡ってくるが、requiredなキーとnot requiredなキーが混在している」ケースなど。公開APIを作っていたらいくらでもありそうなケースですね。
Optional
というそれっぽい型がPythonにはありますが、TypeScriptのOptional Propertyとは少々挙動が異なります。Optional[bool]
と書いた場合、その返り値はbool or undefined
ではなく、bool or None
になります
from typing import Optional, TypedDict
class IMyDict(TypedDict):
age: int
name: str
has_PC: Optional[bool]
my_dict: IMyDict = get_dict()
print(my_dict["has_PC"]) # => bool or None
「has_PCは絶対あるけどその値はboolかNone」は表現できても「has_PCはないかもしれない(あったらbool)」が表現できない、という感じです。
Totality
型定義時にtotal=False
をTypedDictと一緒に渡してあげることで、「すべてのキーが存在するかもしれないし存在しないかもしれない辞書」を定義することができます。
from typing import Optional, TypedDict
class IMember(IMemberRequired, total=False):
last_name: str
first_name: str
middle_name: str
age: int
member: IMember = get_dict()
print(member["middle_name"]) # Could not access item in TypedDict "middle_name" is not a required key in "IMember", so access may result in runtime exception
print(member.get("middle_name", "") # OK
middle_name
に関しては「存在しないかもしれない」を表現することができました。["middle_name"]
でのアクセスはできなくなるので、.get("middle_name", default)
を代わりに使いましょう。
「全部のキーがOptionalな辞書」を作ることができました。次は「一部Requiredで一部Optionalな辞書」を作りたいです。
まとめ
ということで冒頭のコードです。
from typing import TypedDict
class IMemberRequired(TypedDict):
"""
Required keys
"""
last_name: str
first_name: str
class IMember(IMemberRequired, total=False):
"""
Optional keys
"""
middle_name: str
age: int
member: IMember = get_member()
print(member["last_name"]) # => アラートなし
print(member["middle_name"]) # => アラートあり
Requiredなキーだけの辞書定義をまず作って、それを継承する形でoptionalなkeyをもつ辞書を定義することでやりたいことを表現できます。
TypedDictについて記載されたPEP589のTotalityの項をみると以下のように記述されており、これで正当なアプローチと考えて良さそうです。
The totality flag only applies to items defined in the body of the TypedDict definition. Inherited items won't be affected, and instead use totality of the TypedDict type where they were defined. This makes it possible to have a combination of required and non-required keys in a single TypedDict type.
おわり