LoginSignup
10
3

More than 1 year has passed since last update.

PythonのTypedDictでkeyをOptional(undefinedかもしれない)にする

Last updated at Posted at 2021-07-26

やりたいこと

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を使っています

アラート
スクリーンショット 2021-07-27 0.13.05.png

説明

TypedDictについて

Pythonで辞書を型定義するときには、まずdict[x,y]が検討されます。
※ python3.9からリストや辞書の型定義には、typingからimportされるListDictではなく、組み込みのlistdictを使うことが推奨されています(参照

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を導入しています)
スクリーンショット 2021-07-27 0.58.53.png

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.

おわり

10
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
3