Python/構造体から再帰的にインスタンス化するファクトリに関するメモ
はじめに
ここで対象としているのは Python 3.9 となります。TypeScript の経験があるため、それと比較しながら理解を進めています。Python を触り始めてまだ 2 週間も経っていないので、誤っているところや最適化されていない箇所があるかもしれません。初学者としての備忘録を兼ねているため、細かいところからメモをしています。
やりたいこと
再帰的な構造をもった Dict 型の値を、再帰的な構造を持ったユーザ定義のクラスとしてインスタンス化することを目標とします。
ユーザ定義のクラスは from models import xxx
で import できるとします。
またユーザ定義クラスのメンバ変数は、以下のいずれかの型、ないしその Optional 型であるとします。
- ユーザ定義クラス
- プリミティブ型 (
Union[int, float, str, bool]
) - プリミティブ型の List 型 (
List[Union[int, float, str, bool]]
)
例
class Foo:
def __init__(
self,
*,
Bar: str,
):
self.Bar = Bar
class Baz:
def __init__(
self,
*,
Foo: Foo,
Qux: Optional[int] = None,
):
self.Foo = Foo
if Qux is not None:
self.Qux = Qux
material = {
"Baz": {
"Foo": {
"Bar": "bar",
},
"Qux": 100,
},
}
created: Baz = DynamicFactory[Baz].create(material)
Python では PEP 8 で変数は snake_case で命名すべきとされており、
クラスのメンバ変数も同様であることが一般的ですが、ここでは便宜的に PascalCase で定義しています。
型に対する操作について
Optional[T]
を T
に変換する
👉 ■ TypeScript でいうと
やりたいことは typescript でいう NonNullable<T>
のような操作です。
■ 実装例
def non_nullable(arg: Optional[T]) -> T:
arg_tuple = get_args(arg)
if get_origin(arg) is Union and type(None) in arg_tuple:
return arg_tuple[0]
return arg
cf. https://stackoverflow.com/a/58841311
冗長に書けば引数は Union[T, Optional[T]]
ですが T ⊂ Optional[T]
なので Optional[T]
のみで十分です。
typing.Union
■
Union[T, None]
は T
または None
いずれかの型であることを表します。
( python 3.10 からは Union[T, None]
は T | None
書くこともできます )
typing.Optional
■
ドキュメントにもあるように Optional[T]
は Union[T, None]
と同じ評価をされます。
typing.get_args(tp)
, typing.get_origin(tp)
■
get_origin(Union[T, None])
は Union
を返し、
get_args(Union[T, None])
は Tuple[T, None]
を返します。
上の実装例では get_origin(arg) is Union
が True
ならば、
get_args(arg)
が Tuple[T, None]
であることは自明なため [0]
を返しています。
typing.Tuple, typing.List
■
Tuple は実装例には出てきませんが、本文中には登場しているため、ここで触れます。
Tuple[T]
は T 型の要素が 1 つのいわば固定長で、
Tuple[T, K]
は 0 番目の要素が T型, 1 番目の要素が K 型の固定長です。
Tuple[T, K, ...]
0 番目の要素が T型, 1 番目以降の要素が K 型の可変長です。
List[T]
は T 型の要素からなる可変長となります。
test_list: List[str] = ['one', 'two', 'three']
test_tuple: Tuple[str, ...] = ('one', 'two', 'three')
object[T]
を T
に変換する
👉 ■ TypeScript でいうと
TypeScript ではできないみたいです。。。?
cf. https://stackoverflow.com/q/67114094
■ 実装例
def infer_class_generic(arg):
arg_tuple = get_args(arg)
if len(arg_tuple) == 1:
return arg_tuple[0]
raise ValueError
Optional[List[T]]
を T
に変換する
👉 ■ TypeScript でいうと
typescript でいうと以下のような操作です。
type InferListElement<T> = NonNullable<T> extends Array<infer K> ? K : never
type Example = InferListElement<Array<number> | null> // number
■ 実装例
def infer_list_element(arg: Optional[List[T]]) -> T:
return infer_class_generic(non_nullable(arg))
cf. https://stackoverflow.com/a/58841311
non_nullable(arg)
すれば arg が List[T]
であり、
すなわち get_args(arg)
が Tuple[T]
であることは自明なため infer_class_generic(...)
をコールしています。
reflection のような操作について
👉 動的に、インスタンス化せずに、クラスの情報を取得する
importlib.import_module(name, package=None)
■ モジュールを指定して動的に import できます。
getattr(object, name[, default])
■ import されたモジュールからクラスの情報を取得できます。
import importlib
imported_class = getattr(
importlib.import_module("models"),
"Baz"
)
上記は、下記のコードを動的に実行していると考えることができます。
from models import Baz
imported_class = Baz
なお同じ module に定義されているクラスの情報を、同様に取得するには globals()
を使用する方法があります。
imported_class = globals()["Baz"]
👉 クラスの情報から、指定したメソッドの指定した引数の型を取得する
■ 実装例
def inspect_method_param_type(target_method, key: str):
parameters = inspect.signature(target_method).parameters
return parameters.get(key).annotation
inspect
■
inspect.Signature(...)
の引数にメソッドを渡せば、
インスタンス化された Signature
クラスのメンバー変数 parameters
から、
そのメソッドの引数に関する情報である Parameter
クラスのコレクションが取得できます。
引数の名前をキーにしてコレクションから要素を取り出し、 Parameter.annotation
からその引数の型アノテーションが取得できます。
import importlib
import inspect
imported_class = getattr(
importlib.import_module("models"),
"Baz"
)
inspected_parameters = inspect.signature(imported_class.__init__).parameters
annotated_type = inspected_parameters.get("Foo").annotation # Foo
object.__init__(self[, ...])
■ https://docs.python.org/3/reference/datamodel.html#object.__init__
実装例
import importlib
from typing import Any, Dict, Generic, List, Optional, TypeVar
from util.typings import (
infer_class_generic,
infer_list_element,
inspect_method_param_type,
non_nullable,
)
T = TypeVar("T")
K = TypeVar("K")
class DynamicFactory(Generic[T]):
def create(self, convert_from: Dict[str, Any]) -> T:
"""
Dict 型の convert_from を, models で定義された型として再帰的に instantiate する
:param convert_from: 変換元となるデータ
:return: instantiate された T
"""
# T として指定されたクラスの名前を取得する
generic_class_name = infer_class_generic(self.__orig_class__).__name__
return self.__instantiate_recursively(
# 名前空間 model から該当するクラスを import する
getattr(importlib.import_module("models"), generic_class_name),
convert_from,
)
def __instantiate_recursively(
self, target_class: Optional[K], dict_from: Dict[str, Any]
) -> K:
# Optional[K] 型だと後続の処理が面倒になるので、初めに K 型にしておく
required_target_class = non_nullable(target_class)
dict_to: Dict[str, Any] = {}
for key, value in dict_from.items():
# target_class における __init__ の引数のうち、key にマッチする名称の型を取得しておく
target_type = inspect_method_param_type(required_target_class.__init__, key)
if isinstance(value, list):
# List[V] から V の型を取得しておく
element_type = infer_list_element(target_type)
# value が List 型であるときは各要素について再帰的に処理する
carry: List[Any] = []
for element in value:
carry.append(
self.__internal(
element_type,
element,
)
)
dict_to[key] = carry
continue
dict_to[key] = self.__internal(target_type, value)
return required_target_class(**dict_to)
def __internal(self, target_class, value):
if isinstance(value, dict):
# value が Dict 型であるときは型が定義されているとして instantiate して返す
return self.__instantiate_recursively(target_class, value)
# List, Dict いずれの型でないならば primitive な値であるとして instantiate せず、そのまま返す
return value
definition.__name__
■ https://docs.python.org/3/library/stdtypes.html#definition.__name__
__orig_class__
■ https://stackoverflow.com/a/60984681 を参考にしたのですが、公式ドキュメントには記載がなさそう。。。?
Discussion