TypedDictのおさらいとRequired/NotRequired
型付きの辞書TypedDicについて、Python 3.11では辞書の要素ごとに必須・省略可能が指定できるようになりました。
TypedDictとは?
必須と省略可の意義を紹介する前に、TypedDictについておさらいしておきましょう。TypedDictは、Python 3.8で導入された、辞書のキーと値の型を厳密に指定するためのクラスです。TypedDictを継承した辞書では、以下のリストのようにキーnameの値はstr、キーpriceとキーquantityの値はintというように型を明確にできます。
from typing import TypedDict
class Product(TypedDict): (1)
name: str
price: int
quantity: int
product: Product = { (2)
'name': 'MacBook',
'price': 200000,
'quantity': 10,
}
product2: Product = { (3)
'name': 'MacBook',
'price': '200000', # mypyでIncompatible typesエラーとなる
'quantity': 10,
}
(1)でTypedDictのクラスProductを定義しており、(2)はその正しい利用例です。(3)ではキーpriceの値がintであるべきところがstrになっており、これはmypyではIncompatible typesエラーとなります。
TypedDictには、既定ではクラスに列挙された全ての要素が必須という制約があり、一部でも省略すると型チェックでエラーになりますが、クラスの引数にtotal=Falseを与えることで一部が省略可能な辞書とすることはできます。以下のリストはその例です。
# mypyでMissing keyエラーとなる
product3: Product = { (1)
'name': 'MacBook',
'price': 200000,
}
class ProductPart(TypedDict, total=False): (2)
name: str
price: int
quantity: int
product_part: ProductPart = { (3)
'name': 'MacBook',
'price': 200000,
}
(1)ではquantityキーがないのでmypyではMissing keyエラーとなります。(2)のようにtotal=Falseを引数に与えたProductPartクラスでは、(3)のようにquantityキーが省略されたとしてもエラーとはなりません。ただし、nameキーが欠けてもpriceキーが欠けても良いというのがtotal=Falseの動作となるので、意図した省略なのかは静的型チェックでは分かりません。
RequiredとNotRequiredで必須か省略可能かを指定可能
このように、要素の全てが必須(total=True)、あるいはどれかあるいは全部が欠けていてもよい(total=False)というように極端で、省略されてもそれが意図的なものなのか分からないため、Pythin 3.11では辞書の要素ごとに必須、省略可能を指定できるようになりました。それが、typing.Requiredとtyping.NotRequiredです。Required型修飾子が付いたフィールドは必須となり省略できず、NotRequired型修飾子が付いたフィールドは省略可能になります。以下のリストでは、quantityのみが省略可能になります。
from typing import TypedDict, Required, NotRequired
class Product(TypedDict): (1)
name: Required[str]
price: Required[int]
quantity: NotRequired[int] # 省略可能
…略…
# エラーにならない
product2: Product = { (2)
'name': 'MacBook',
'price': 200000,
}
(1)は、RequiredとNotRequiredをフィールドに指定したクラスの定義です。なお、各フィールドの必須の有無の既定はtotal引数の指定に準ずるので、total=True(既定)なら全フィールド必須、total=Falseなら全フィールド省略可能になります。
(2)ではquantityキーを省略していますが、NotRequiredが指定されているのでエラーとはなりません。意味的にも明確になります。
TypedDictのジェネリクス
TypedDictに関連して、TypedDictのジェネリクス対応についても紹介しておきます。指定できる型は1個ですが、外部から要素の型を指定できるTypedDictが利用できます。以下のリストは、type要素を型引数Tで指定できることで、対応する値をstrでもintでも表現できるようにした例です。
from typing import TypeVar, Generic, TypedDict
T = TypeVar("T")
class Product(TypedDict, Generic[T]): (1)
name: str
type: T
product_str: Product[str] = { (2)
'name': 'MacBook',
'type': 'PC',
}
product_int: Product[int] = { (3)
'name': 'MacBook',
'type': 101,
}
(1)が、ジェネリックを用いたTypedDictの定義です。従来、TypedDictと非TypedDictなクラスを同時に継承することは許可されていませんでした。しかし、Python 3.11ではこの制限が解消されたことで、ジェネリクスが実現されました。
(2)と(3)は、それぞれ型引数をstr、intとしたインスタンス生成の例です。確かに、異なる型の要素を使い分けられることが確認できます。
LiteralString型により関数引数などに文字列リテラルのみの指定が可能に
型ヒントの一つに、特定の文字列定数のみが有効というLiteralがあります。例えば、文字列リテラル"HTTP"のみを受け入れる場所にはLiteral("HTTP")と型ヒントを与えることで、それ以外の文字列定数は使えなくなります。値が決まっていれば有用な型ヒントになりますが、文字列定数である必要はあるけど中身は何でも良い、という場合には利用できません。
そこでPython 3.11ではLiteralString型が導入されて、任意の文字列定数を受入れ可能なことを明示できるようになりました。これは例えば関数がSQL文を引数として受け入れる場合に、それが定数であることを保証する際などに有用です。外部でSQL文が生成されて渡されるとすると、SQLインジェクションなどのセキュリティリスクを生じる可能性があるからです。このようなときに定数であることを保証すれば、危険なSQL文が渡されるリスクを低減できます。以下のリストは、これを単純な形で表してみた例です。
from typing import LiteralString
# 検索語句を必ず文字列リテラルで受け取る関数
def db_access(s: LiteralString) -> bool: (1)
print(f"Execute: SELECT * FROM table WHERE name = '{s}'")
return True
lstr: LiteralString = 'nao'
astr: str = input() # たとえば nao' OR '1' = '1
db_access(lstr) (2)
db_access(astr) # Pylanceでエラー
(1)は、検索語句をLiteralString型の引数で受け取り、SQLのSELECT文を発行するということを想定した関数の定義です。実際にはSQL文を表示しているだけです。
(2)以降は、その呼び出し例です。LiteralStringであるlstrを渡す呼び出しには問題ないですが、strであるastrを渡している方はPylanceで「"astr" は "LiteralString" と互換性がありません」のエラーとなります(mypyではサポートされないようでスルーされます)。
このように、引数をLiteralStringに制限することで、外部入力を渡してしまうといったコードを実行前に検出することができます。
