Python クラスボディー評価

以下のことには解決法(記事末)が見つかったので、
備忘録として残しておく


class A:
    var = 2

    class B:
        var = A.var  # NameError: name 'A' is not defined
        var = var  # NameError: name 'var' is not defined

こうなる理由は
上: Aの参照が確立する前にBが定義されるから
下: クラスはエンクロージングスコープを形成しないから

Aのメタクラス(デフォルトはtype)の__new__からインスタンスが吐かれて初めて参照が発生する。これについて、上から順に仮の参照として設定していってほしいとおもう。なぜなら階層シングルトン(モノステート)をクラスの即時評価を利用してつくるとき、共通化のためにメタクラスを以下のように使おうとすると

class A:

    class meta(type):
        pass
    
    class B(metaclass=meta):  # OK
        
        class C(metaclass=meta):  # NameError: name 'meta' is not defined
            pass

となり、これを解決するために

class A:
    
    class meta(type):
        pass
    
    class B(metaclass=A.meta):  # NameError: name 'A' is not defined
        
        class C(metaclass=A.meta):  # NameError: name 'A' is not defined
            pass

とすることはできないからだ。
ところでクラスのボディの評価は特殊で、

var = 2

class A:
    var = var
    var += 1

print(A.var)  # 3


def fn():
    var = var  # UnboundLocalError: local variable 'var' referenced before assignment
    fn.var = var + 1
    print(var)  # 2
    print(fn.var)  # 3

fn()

これは A.var = var として解釈され以降はvarがA.varと同様に解釈し続けるからである(厳密にはAの参照はまだ存在しないので、A.varと書くとエラーが出る)。p.s.参照されるvarはグローバル変数である必要がある。グローバル変数以外だと名前が被ったときUndefinedエラーがでてくる。グローバル特権

したがって解決するには

class meta(type):
    pass

class A:
    
    class B(metaclass=meta):
        
        class C(metaclass=meta):
             pass

こうしなければならない。これをもし深いネストでマイナーチェンジしたメタクラスを使おうものなら外の空間にクラス内でしか使わないようなfactoryを置かないといけなくなる。また一般のクラス変数についても同様に外に出す必要があるし、ネストコンテナの一部をトリクルダウンしていくときはこれまたfactoryでも使わないといけない。

p.s.メタクラスを使わずとも__init_subclass__と継承を使えばより柔軟に対応できる。ただ依然として外に置く必要がある。GPTがポロッと話してくれた


結論 解決.

def f():

    def _construct(scope: dict) -> Composite:  # 名前の衝突をさけるためのアンダーバー
        return Composite(**{k[1:] if k.startswith('_') else k: v() if callable(v) else v 
                        for k, v in scope.items() if k != '_construct'})

    val = 2

    def f1():

        def f11():
            some_leaf_args = hoge
            return _construct(vars())  
        
        def f12():
            some_leaf_args = hoge
            _val = val  # エンクロージングスコープのvalを参照
            return _construct(vars())

        return _construct(vars())

    return _construct(vars())
 
composite_inst = f()

ただしreturnが冗長

いいなと思ったら応援しよう!