Slide 1

Slide 1 text

OSS を読んで勉強するソフトウェア開発 NamedTuple 編 Licensed under CC BY-SA 4.0 1

Slide 2

Slide 2 text

自己紹介 できること C++, Python, Typescript/React お仕事 生産技術 Machine Learning 生息地 Twitter: https://twitter.com/elda277 Blog: https://elda27.hatenablog.com/ Licensed under CC BY-SA 4.0 2

Slide 3

Slide 3 text

Agenda 前置き NamedTuple について/目標 実践 Licensed under CC BY-SA 4.0 3

Slide 4

Slide 4 text

前置き Licensed under CC BY-SA 4.0 4

Slide 5

Slide 5 text

仕事中のあるある 仕事中の三大 FAQ ライブラリの使い方が動きません! ⇒ ドキュメント読め ドキュメント読んでも動きません! ⇒ ソース読め バグが―― ⇒ ソース... とは言え、自分以外が書いたソースコードを読むためには訓練が必要 そうだ OSS を読もう Licensed under CC BY-SA 4.0 5

Slide 6

Slide 6 text

OSS を読むとイイゾ プログラミング力 設計の手札が増える 保守性が高いコードの書き方がわかる デバッグ力 より低レイヤの気持ちが分かるようになる 良いテストの書き方が分かる その他 書籍には書かれていない力が得られる 困ったときに自分で書いて解決できる Licensed under CC BY-SA 4.0 6

Slide 7

Slide 7 text

読もうと思って読めれば苦労しないけど、どうするか? ソースコードを読む前の準備 ドキュメントを読む ライブラリやツールの使い方を熟知する ⇒ システムの仕組みを知っておくとソースコードの流れが分かる OSS を読む方針 全部を読もうとしない 目的を決めて読む ⇒ 全てを理解することは大変で勉強のレベルに留まらなくなる Licensed under CC BY-SA 4.0 7

Slide 8

Slide 8 text

企画の趣旨 OSS を読めと言う人は多いが、実際読んだ経験とか記録は少ない 私も読むと良いよとよく言う 1 人だけど自分で読んだ記録はつけたことない 多分、マニアック過ぎて読者層が限定されすぎるからみんな書きたくない (Impression 低そう) 私自身、最近 Output する気力が無くてとりあえずやってみようと思った ちなみにスライドにしたのはブログ記事として書くだけの文字数を書くのが辛かった から 実際作ってみて分かったけどスライドもしんどい Licensed under CC BY-SA 4.0 8

Slide 9

Slide 9 text

NamedTuple について/目標 Licensed under CC BY-SA 4.0 9

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

もう一つの書き方 個人的にはこっちしか使わない 記法も一般的なクラス定義で 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

Slide 12

Slide 12 text

何が便利か エディタで型ヒントが見える 似たようなライブラリであるpydantic は見えない なにの差? Licensed under CC BY-SA 4.0 12

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

ソースを読む目的 1. エディタで型ヒントが見える仕組みについて学ぶ 2. Python stdlib の読み方を習熟する Licensed under CC BY-SA 4.0 14

Slide 15

Slide 15 text

実践 Licensed under CC BY-SA 4.0 15

Slide 16

Slide 16 text

とりあえず 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

Slide 17

Slide 17 text

そもそも pyi ファイルって? PEP 561 で定義される型ヒントを記述するファイル 型ヒントのみを記述したいわゆる stub ファイルのみを提供することが可能 以下が詳しい https://blog.ymyzk.com/2018/09/creating-packages-using-pep-561/ つまり実装ではない Licensed under CC BY-SA 4.0 17

Slide 18

Slide 18 text

直接ソースを読む 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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

__annotations__ とは 型ヒントの定義のディクショナリ この変数に戻り値・引数などの値を ちなみに動的に変更可能で今回のケースはこっち >>> def test(a: int, b: int)->None: >>> pass >>> test.__annotations__ {'a': int, 'b': int, 'return': None} Licensed under CC BY-SA 4.0 20

Slide 21

Slide 21 text

これで完璧だね? 実はあまり何も解決していない 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

Slide 22

Slide 22 text

別解: 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

Slide 23

Slide 23 text

もうちょっと 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

Slide 24

Slide 24 text

_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

Slide 25

Slide 25 text

type.__new__ によるメタクラスの設定 type.__new__ はメタクラスの設定は以下の手順で行う type.__new__ でメタクラスを生成する __mro_entries__ を本来の基底クラスで置き換える __mro_entries__ とは PEP560によると metaclass の Overhead を低減するための仕組みの 1 つらしい __mro__ に登録するクラスを返す Licensed under CC BY-SA 4.0 25

Slide 26

Slide 26 text

そもそもメタクラスとは メタプログラミングのための仕組み メタプログラミングは別のロジックに基づいてプログラムの実行前に処理を置き換え る スクリプト言語の場合 import したタイミングで処理を置き換えられる Python のメタクラスとは __new__ によって型の生成処理を行う 通常のクラスは type.__new__ によって行っている Licensed under CC BY-SA 4.0 26

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

pydantic.BaseModel`を改変した Before/After (左) Before / (右) After Licensed under CC BY-SA 4.0 28

Slide 29

Slide 29 text

まとめ OSS を読むとツヨツヨになれる プログラマーとしての力が強まる 困ったときに自分で実装を修正できる 今回のポイント __annotations__ には引数の型ヒントが入っている メタクラスを使うとクラス定義時の情報を置き換え可能 Type hint を動的に生成したいときは以下を組み合わせれば良い(pydantic の例の場合) meta class を定義する その中で __annotations__ を動的に生成する 後は __defaults__ と __kwdefaults__ とかも設定するとなお良い Licensed under CC BY-SA 4.0 29

Slide 30

Slide 30 text

Appendix Licensed under CC BY-SA 4.0 30

Slide 31

Slide 31 text

補足 1:MRO(Method Resolution Order)とは Python ではメソッドの解決を __mro__ に登録されたクラスの順番で行う 多重継承すると __mro__ には記載した順番で登録される ちなみに Readonly なので動的に書き換えると AttributeError が出る >>> class A: >>> pass >>> class B: >>> pass >>> class C(B, A): >>> pass >>> print(C.__mro__) (, , , ) Licensed under CC BY-SA 4.0 31

Slide 32

Slide 32 text

補足 2 : __mro_entries__ のサンプル かなり邪悪なコードだけどこういうことができる どこにも __init__ と upper を定義していないけど MRO の 2 番目に が 入っているので str のメンバ関数を呼び出せる >>> class _StrDummy: >>> def __mro_entries__(self, bases): >>> return (str,) >>> StrDummy = _StrDummy() >>> class B(StrDummy): >>> pass >>> print(B.__mro__) (, , ) >>> print(B('int').upper()) INT Python の標準ライブラリでは typing.GenericAlias とかで使われている Licensed under CC BY-SA 4.0 32