triggon 遅延クラスの実装

【個人ブログ】開発過程と学習記録

投稿日:


遅延クラスの TrigFunc は、主に __getattr____call__ を利用して実装しています。
現在開発中ですが実際のコードを引用して解説していきます!

属性チェーンと引数の記録

__getattr__ で属性アクセスのチェーンを、__call__ で引数を記録します。
この遅延クラスでは、属性アクセスや呼び出しのたびに記録を保持し、毎回新しい TrigFunc インスタンスを返します。これはチェーンの長さが事前に分からないので、元の状態を壊さずに安全にチェーンを伸ばせるようにするためです。

    def __call__(self, *args: Any, **kwargs: Any) -> Self:
        if self._trigcall is None:
            raise TypeError("TrigFunc instance is not bound to a callable")

        new_trigcall = self._trigcall.add_call(args, kwargs) # 引数を記録

        return type(self)._clone_with(
            new_trigcall,
            self._f_locals,
            self._f_globals,
        )

    def __getattr__(self, name: str) -> Self:
        if self._trigcall is None:
            new_trigcall = _TrigCall((("attr", name),), name)
        else:
            new_trigcall = self._trigcall.add_attr(name) # 属性アクセスを記録

        return type(self)._clone_with(
            new_trigcall,
            self._f_locals,
            self._f_globals,
        )

フレーム検索による名前解決

そして、実行時にフレーム検索で最初の名前を解決し、その後に属性が続く場合は getattr でオブジェクトを取得し、呼び出しの場合は保存しておいた引数を入れて実行結果を取得します。

    def run(self) -> Any:
        if self._trigcall is None:
            raise TypeError("no deferred target to execute")

        obj = _NO_VALUE

        for v in self._trigcall.target:
            if v[0] == "attr":
                name = v[1]
                try:
                    if obj is _NO_VALUE:
                        obj = self.resolve_value(name)
                    else:
                        obj = self.resolve_value(name, obj)
                except NameError:
                    # 再度呼び出し元のフレームを取得
                    try:
                        frame = get_target_frame()
                        if obj is _NO_VALUE:
                            obj = self.resolve_value(name, frame=frame)
                        else:
                            obj = self.resolve_value(name, obj, frame=frame)
                    finally:
                        frame = None
            elif v[0] == "call":
                args, kwargs = v[1], v[2]
                obj = cast(Callable[..., Any], obj)(*args, **kwargs)
            else:
                raise AssertionError(f"unreachable kind key: {v[0]!r}")

        return obj

ローカルとグローバルで見つからない場合は、builtins で検索しているので組み込み関数などにも対応しています。

さらに TrigFunc インスタンスを別のスコープでも使えるようにもしました!
最初に呼ばれたフレームの f_globalsf_locals を取得してインスタンス属性に保存し、もしそのスコープ内で名前が見つからない場合(NameError)は、再度呼び出し元のフレームを取得して再解決することで実現しています。

TrigFunc の使い方

遅延クラスを実装するにあたって自然に使えるように意識しました。
先頭にインスタンス変数を付けてあとは遅延対象の関数やメソッドをいつも通り呼ぶだけです。

class Main:
    def method(self, num):
        return num * 10


f = TrigFunc()
result = f.Main().method(10)
print(result) # TrigFuncインスタンス

tg = Triggon.from_label("A", new_values=f.Main().method(10)) # ラベルと更新値を登録

tg.set_trigger("A") # ラベル'A'を有効化

a = tg.switch_lit("A", original_val=0) # ラベル'A'が有効時に更新値に切り替える
print(a) # 100

更新値が TrigFunc で遅延されている場合は内部で実行して返す仕様になっています。

今回の改良で使いやすさと柔軟性がかなり上がったと思います。
引用したコードはGitHubに載っているので興味ある方は見てみてください!

コメント

タイトルとURLをコピーしました