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が冗長