定义抽象类属性时NotImplementedError意外触发的原因排查
抽象类属性实现中NotImplementedError意外触发问题
之前基于自定义的classProp和ClassPropMeta实现了类属性机制,尝试结合@abstractmethod定义子类必须实现的抽象类属性时,运行代码意外触发了NotImplementedError。调试发现cls参数指向类B,调用栈显示触发点关联abc模块的_abc_init流程,但无法定位B.X的具体调用时机。
最小复现代码
from abc import ABCMeta, abstractmethod from typing import Any class classProp[T, P](property): def __get__(self, instance: T, owner: type[T] = None) -> P: return self.fget(owner) class ClassPropMeta(ABCMeta): def __setattr__(cls, name: str, value: Any): if isinstance(desc := vars(cls).get(name), classProp) and not callable( desc.fset ): raise AttributeError("can't set attribute") return super().__setattr__(name, value) class A(metaclass=ClassPropMeta): __slots__ = () @classProp @abstractmethod def X(cls): # breakpoint() raise NotImplementedError class B(A): ... # @classProp # def X(cls): # return 1 class C(B): @classProp def X(cls): return 1 print(C.X) # 运行结果:触发NotImplementedError
调用栈信息
> d:\path\to\my\example.py(31)X() -> breakpoint() (Pdb) w d:\path\to\my\example.py(35)<module>() -> class B(A): <frozen abc>(107)__new__() d:\path\to\my\example.py(13)__get__() -> return self.fget(owner) > d:\path\to\my\example.py(31)X() -> breakpoint()
问题原因
ABCMeta在创建子类(如B)时,会执行抽象成员的初始化校验流程,在此过程中会触发classProp的__get__方法。由于classProp的__get__直接调用了父类A中标记为抽象的X方法,而该方法内部硬编码了NotImplementedError,导致子类创建阶段就抛出错误,而非由abc机制正常处理抽象成员的未实现校验。
同时,classProp未被abc识别为抽象成员载体,导致abc无法正确判断子类是否已实现该属性,反而触发了属性访问逻辑。
解决方案
方案1:调整抽象类属性的错误抛出逻辑
移除抽象方法内的NotImplementedError,让abc的原生机制处理抽象成员校验;同时让classProp携带抽象标记,被abc识别:
from abc import ABCMeta, abstractmethod from typing import Any class classProp[T, P](property): def __get__(self, instance: T, owner: type[T] = None) -> P: return self.fget(owner) def __init__(self, fget=None, fset=None, fdel=None, doc=None): super().__init__(fget, fset, fdel, doc) # 传递抽象方法标记,让abc识别该描述符为抽象成员 if fget and hasattr(fget, '__isabstractmethod__'): self.__isabstractmethod__ = fget.__isabstractmethod__ class ClassPropMeta(ABCMeta): def __setattr__(cls, name: str, value: Any): if isinstance(desc := vars(cls).get(name), classProp) and not callable( desc.fset ): raise AttributeError("can't set attribute") return super().__setattr__(name, value) class A(metaclass=ClassPropMeta): __slots__ = () @classProp @abstractmethod def X(cls): # 移除手动抛出的错误,由abc机制处理 pass class B(A): ... # 未实现X,B会被标记为抽象类,无法实例化,但类本身可以正常定义 # @classProp # def X(cls): # return 1 class C(B): @classProp def X(cls): return 1 print(C.X) # 输出:1 # 若尝试实例化B:b = B(),会抛出TypeError: Can't instantiate abstract class B with abstract method X
方案2:修改classProp的__get__逻辑,避免子类初始化阶段触发父类方法
通过检查子类是否已实现该属性,避免在abc校验流程中调用父类的抽象方法:
from abc import ABCMeta, abstractmethod from typing import Any class classProp[T, P](property): def __get__(self, instance: T, owner: type[T] = None) -> P: if owner is None: owner = type(instance) # 检查子类是否已实现该属性 if self.name in owner.__dict__: # 子类已实现,调用子类的方法 return self.fget(owner) # 子类未实现,判断是否为抽象类校验阶段 if getattr(owner, '__abstractmethods__', None) and self.name in owner.__abstractmethods__: # 属于抽象校验,抛出AttributeError让abc处理 raise AttributeError(f"Abstract class property {self.name} not implemented") # 其他情况调用父类方法(可选,若需要保留原错误逻辑) return self.fget(owner) def __set_name__(self, owner, name): # 记录属性名称,用于后续检查 self.name = name class ClassPropMeta(ABCMeta): def __setattr__(cls, name: str, value: Any): if isinstance(desc := vars(cls).get(name), classProp) and not callable( desc.fset ): raise AttributeError("can't set attribute") return super().__setattr__(name, value) class A(metaclass=ClassPropMeta): __slots__ = () @classProp @abstractmethod def X(cls): raise NotImplementedError class B(A): ... class C(B): @classProp def X(cls): return 1 print(C.X) # 输出:1
内容的提问来源于stack exchange,提问作者F. X. P.
相关产品推荐
相关产品推荐

