Upgrade to Pro — share decks privately, control downloads, hide ads and more …

OSSを読む NamedTuple編

Avatar for elda27 elda27
September 26, 2021

OSSを読む NamedTuple編

Licensed under the CC BY-SA 4.0

OSS を読むとツヨツヨになれる

プログラマーとしての力が強まる
困ったときに自分で実装を修正できる

Type hint を動的に生成したいときは以下を組み合わせれば良い

meta class を定義する
その中で__annotations__を動的に生成する
後は__defaults____kwdefaults__とかも設定するとなお良い
番外編だけど Pyhton 3.10 移行は__signature__を設定しも良い

Avatar for elda27

elda27

September 26, 2021
Tweet

More Decks by elda27

Other Decks in Technology

Transcript

  1. 自己紹介 できること C++, Python, Typescript/React お仕事 生産技術 Machine Learning 生息地

    Twitter: https://twitter.com/elda277 Blog: https://elda27.hatenablog.com/ Licensed under CC BY-SA 4.0 2
  2. 仕事中のあるある 仕事中の三大 FAQ ライブラリの使い方が動きません! ⇒ ドキュメント読め ドキュメント読んでも動きません! ⇒ ソース読め バグが――

    ⇒ ソース... とは言え、自分以外が書いたソースコードを読むためには訓練が必要 そうだ OSS を読もう Licensed under CC BY-SA 4.0 5
  3. 企画の趣旨 OSS を読めと言う人は多いが、実際読んだ経験とか記録は少ない 私も読むと良いよとよく言う 1 人だけど自分で読んだ記録はつけたことない 多分、マニアック過ぎて読者層が限定されすぎるからみんな書きたくない (Impression 低そう) 私自身、最近

    Output する気力が無くてとりあえずやってみようと思った ちなみにスライドにしたのはブログ記事として書くだけの文字数を書くのが辛かった から 実際作ってみて分かったけどスライドもしんどい Licensed under CC BY-SA 4.0 8
  4. NamedTuple とは 言語:Python 一言で言えば:名前付きのタプル/C で言うところの構造体 >>> from collections import namedtuple

    >>> Collection = namedtuple('Collection', ['any_string', 'any_value']) >>> c = Collection('hoge', 10) >>> print(c.any_string) hoge >>> print(c.any_valie) 10 dictionary はエディタによる補完が効かないなど設定値など宣言的に行いたい場合に不便 いちいちクラスで型を作るより簡単に作れるのでよく使う。 Licensed under CC BY-SA 4.0 10
  5. もう一つの書き方 個人的にはこっちしか使わない 記法も一般的なクラス定義で C の構造体と親しく良い Type hint も付けられるので非常に便利 >>> from

    typing import NamedTuple >>> class Collection(NamedTuple): >>> any_string: str >>> any_value: int >>> c = Collection('hoge', 10) >>> print(c.any_string) hoge >>> print(c.any_valie) 10 Licensed under CC BY-SA 4.0 11
  6. pydantic とは documentによると Data validation and settings management using python

    type annotations. pydantic enforces type hints at runtime, and provides user friendly errors when data is invalid. Define how data should be in pure, canonical python; validate it with pydantic. つまり設定の管理とデータの検証を type annotation によって行うライブラリ 値の変換を定義に基づいて自動で実施 (例えば pathlib.Path <-> str) JSON or JSON Schema への serialize/deserialize Web 系の開発にすごく便利 ⇒ Fast API でも使われている Licensed under CC BY-SA 4.0 13
  7. とりあえず NamdeTuple の定義を読む VS Code の型定義参照(Ctrl+右クリック)でとりあえず定義へと飛ぶ typing.pyi: 665 行目~ 677

    行目 class NamedTuple(Tuple[Any, ...]): _field_types: collections.OrderedDict[str, Type[Any]] _field_defaults: dict[str, Any] _fields: Tuple[str, ...] _source: str def __init__(self, typename: str, fields: Iterable[Tuple[str, Any]] = ..., **kwargs: Any) -> None: ... @classmethod def _make(cls: Type[_T], iterable: Iterable[Any]) -> _T: ... if sys.version_info >= (3, 8): def _asdict(self) -> dict[str, Any]: ... else: def _asdict(self) -> collections.OrderedDict[str, Any]: ... def _replace(self: _T, **kwargs: Any) -> _T: ... 実装がねえ Licensed under CC BY-SA 4.0 16
  8. 直接ソースを読む typing.py#L2143 どうやら NamedTuple の実装は collections.namedtuple そのものらしい 実際に Type hint

    を付与しているのは 2218 行目 NamedTuple はこの後で紆余曲折あって関数とは言えない何かになっている 2212 def _make_nmtuple(name, types, module, defaults = ()): 2213 fields = [n for n, t in types] 2214 types = {n: _type_check(t, f"field {n} annotation must be a type") 2215 for n, t in types} 2216 nm_tpl = collections.namedtuple(name, fields, 2217 defaults=defaults, module=module) 2218 nm_tpl.__annotations__ = nm_tpl.__new__.__annotations__ = types 2219 return nm_tpl ... 2256 def NamedTuple(typename, fields=None, /, **kwargs): ... 2890 return _make_nmtuple(typename, fields, module=module) Licensed under CC BY-SA 4.0 18
  9. namedtuple における今回のポイント collections.__init__.py#L328:ポイント __new__ の定義は eval で Type hint 含む引数リストを生成している。

    type で tuple を基底クラスとする型を生成 328 def namedtuple(typename, field_names, *, rename=False, defaults=None, module=None): ... 399 arg_list = ', '.join(field_names) ... 413 code = f'lambda _cls, {arg_list}: _tuple_new(_cls, ({arg_list}))' 414 __new__ = eval(code, namespace) 464 class_namespace = { ... 469 '__new__': __new__, ... 476 } ... 481 result = type(typename, (tuple,), class_namespace) Licensed under CC BY-SA 4.0 19
  10. これで完璧だね? 実はあまり何も解決していない class A(Base): # Baseは定義されたクラス変数から__init__を作る a: int b: str

    # どうやって以下をクラス定義と同時に設定するか? A.__init__.__annotations__ = { 'a': int, 'b': str, } 結局 __annotations__ を適切に設定する方法が分かっていない Licensed under CC BY-SA 4.0 21
  11. 別解: Decorator Decorator を使った代わりの方法 比較的シンプルに処理できる def modify_init(klass): def generator(**kwargs): return

    klass(**kwargs) klass.__init__ = generator klass.__init__.__annotations__ = klass.__annotations__ return klass @modify_init class A(Base): a: int b: str Licensed under CC BY-SA 4.0 22
  12. もうちょっと NamedTuple を深堀りする typing.py#L2256:ポイント _NamedTuple は type.__new__ で生成している 関数で定義された NamedTuple.__mro_entries__

    を置き換えている 2256 def NamedTuple(typename, fields=None, /, **kwargs): ... 2291 _NamedTuple = type.__new__(NamedTupleMeta, 'NamedTuple', (), {}) 2292 2293 def _namedtuple_mro_entries(bases): ... 2297 return (_NamedTuple,) 2298 2299 NamedTuple.__mro_entries__ = _namedtuple_mro_entries Licensed under CC BY-SA 4.0 23
  13. _NamedTuple は type.__new__ で生成している 2291 _NamedTuple = type.__new__(NamedTupleMeta, 'NamedTuple', (),

    {}) __new__ はインスタンスを生成 type.__new__ ^は新しい型を生成 上の場合 NamedTupleMeta を NamedTuple という型名で生成 ここでは基底クラスの設定をしていないが実際には存在している def __new__( cls: Type[_TT], name: str, bases: Tuple[type, ...], namespace: dict[str, Any], **kwds: Any ) -> _TT: ... Licensed under CC BY-SA 4.0 24
  14. type.__new__ によるメタクラスの設定 type.__new__ はメタクラスの設定は以下の手順で行う type.__new__ でメタクラスを生成する __mro_entries__ を本来の基底クラスで置き換える __mro_entries__ とは

    PEP560によると metaclass の Overhead を低減するための仕組みの 1 つらしい __mro__ に登録するクラスを返す Licensed under CC BY-SA 4.0 25
  15. pydantic.BaseModel を改変して使えるようにする main.py#361: pydantic.ModelMetaclass をいじると勝てる 228 def __new__(mcs, name, bases,

    namespace, **kwargs): # noqa C901 ... 361 cls = super().__new__(mcs, name, bases, new_namespace, **kwargs) + 362 if not hasattr(cls, '__init__'): # __init__ is defined at subclass + 363 cls.__init__.__annotations__ = { + 364 k: field.type_ + 365 for k, field in fields.items() + 366 } + 367 cls.__init__.__kwdefaults__ = { + 368 k: field.default for k, field in fields.items() + 369 } + 370 cls.__init__.__kwdefaults__.update({ + 371 field.alias: field.default + 372 for field in fields.values() + 373 if field.has_alias + 374 }) Licensed under CC BY-SA 4.0 27
  16. まとめ OSS を読むとツヨツヨになれる プログラマーとしての力が強まる 困ったときに自分で実装を修正できる 今回のポイント __annotations__ には引数の型ヒントが入っている メタクラスを使うとクラス定義時の情報を置き換え可能 Type

    hint を動的に生成したいときは以下を組み合わせれば良い(pydantic の例の場合) meta class を定義する その中で __annotations__ を動的に生成する 後は __defaults__ と __kwdefaults__ とかも設定するとなお良い Licensed under CC BY-SA 4.0 29
  17. 補足 1:MRO(Method Resolution Order)とは Python ではメソッドの解決を __mro__ に登録されたクラスの順番で行う 多重継承すると __mro__

    には記載した順番で登録される ちなみに Readonly なので動的に書き換えると AttributeError が出る >>> class A: >>> pass >>> class B: >>> pass >>> class C(B, A): >>> pass >>> print(C.__mro__) (<class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>) Licensed under CC BY-SA 4.0 31
  18. 補足 2 : __mro_entries__ のサンプル かなり邪悪なコードだけどこういうことができる どこにも __init__ と upper

    を定義していないけど MRO の 2 番目に <class 'str'> が 入っているので str のメンバ関数を呼び出せる >>> class _StrDummy: >>> def __mro_entries__(self, bases): >>> return (str,) >>> StrDummy = _StrDummy() >>> class B(StrDummy): >>> pass >>> print(B.__mro__) (<class '__main__.B'>, <class 'str'>, <class 'object'>) >>> print(B('int').upper()) INT Python の標準ライブラリでは typing.GenericAlias とかで使われている Licensed under CC BY-SA 4.0 32