diff --git a/lib/vast/Nodes.py b/lib/vast/Nodes.py index 8d8c498..035dd16 100644 --- a/lib/vast/Nodes.py +++ b/lib/vast/Nodes.py @@ -11,7 +11,7 @@ AST 树,从而使编译器侧 ``import vast as ast`` 后原有代码零修改 构造语义与 CPython 一致: - 位置参数按 ``_fields`` 顺序赋值,超出字段数抛 TypeError -- 关键字参数全部 setattr(包括未知字段,与 CPython 行为一致) +- 关键字参数按名赋值(实例为 __slots__ 紧凑布局,仅接受已声明字段) - 未传入的字段不设置属性(访问时 AttributeError,与 CPython 一致) """ from __future__ import annotations @@ -20,6 +20,10 @@ from __future__ import annotations class AST: """所有 AST 节点的基类(与 ast.AST 同构)。""" + # 空 __slots__:保证子类定义 __slots__ 后实例不携带 __dict__ + # (基类若有 __dict__,子类 __slots__ 将失效) + __slots__ = () + _fields: tuple[str, ...] = () _attributes: tuple[str, ...] = () @@ -310,22 +314,25 @@ def _MakeNodeClass(name: str, fields: tuple[str, ...], attrs: tuple[str, ...]) - f'{name} constructor takes at most {len(fields)} positional arguments' ) for fname, val in zip(fields, args): - self.__dict__[fname] = val + object.__setattr__(self, fname, val) for k, v in kwargs.items(): - self.__dict__[k] = v + object.__setattr__(self, k, v) base_name: str | None = _BASE_CLASS.get(name) base_cls: type = globals()[base_name] if base_name else AST + # __slots__ = 位置字段 + 位置属性(实例的全部属性名) + slots: tuple[str, ...] = tuple(dict.fromkeys(fields + attrs)) ns: dict[str, object] = { '__init__': _Init, + '__slots__': slots, '_fields': fields, '_attributes': attrs, + # 字段默认值不能直接作为类属性(与同名 __slots__ 冲突), + # 存入独立类属性供 dump 判断"值为 None 且默认值为 None"时跳过 + '_node_defaults': dict(_CLASS_DEFAULTS.get(name, {})), '__module__': __name__, '__doc__': _DOCS.get(name, f'{name} AST 节点(与 ast.{name} 同构)。'), } - # 类默认值(None 字段在 dump 时跳过) - for fname, fdefault in _CLASS_DEFAULTS.get(name, {}).items(): - ns[fname] = fdefault cls: type = type(name, (base_cls,), ns) return cls diff --git a/lib/vast/Utils.py b/lib/vast/Utils.py index 67dc3c7..08e200f 100644 --- a/lib/vast/Utils.py +++ b/lib/vast/Utils.py @@ -81,7 +81,7 @@ def dump(node: AST, annotate_fields: bool = True, include_attributes: bool = Fal except AttributeError: keywords = True continue - if value is None and getattr(cls, name, ...) is None: + if value is None and getattr(cls, '_node_defaults', {}).get(name, ...) is None: keywords = True continue value, simple = _format(value, level) @@ -96,7 +96,7 @@ def dump(node: AST, annotate_fields: bool = True, include_attributes: bool = Fal value = getattr(node, name) except AttributeError: continue - if value is None and getattr(cls, name, ...) is None: + if value is None and getattr(cls, '_node_defaults', {}).get(name, ...) is None: continue value, simple = _format(value, level) allsimple = allsimple and simple