如何用装饰器为SQLAlchemy类下划线属性生成只读@property?
自动为下划线开头属性生成只读@property的问题解决
你的装饰器失效的原因
你写的装饰器在类定义阶段执行,dir(cls)只能遍历类级别的属性(比如类方法、类变量),但测试代码里的_age、_address是在__init__中定义的实例属性——类定义阶段这些属性还不存在,所以dir(cls)根本找不到它们,自然无法生成对应的@property。
修复方案
场景1:属性是类级别(比如SQLAlchemy实体类的Column)
SQLAlchemy实体类的属性通常是用Column定义的类属性,这种场景下可以修改装饰器,遍历类的__dict__来获取下划线开头的类属性:
def make_readonly_properties(cls): # 遍历类属性,跳过双下划线开头的特殊属性(如__init__) for attr_name, _ in cls.__dict__.items(): if attr_name.startswith('_') and not attr_name.startswith('__'): prop_name = attr_name[1:] # 生成只读property,绑定对应的下划线属性 setattr(cls, prop_name, property(lambda self, name=attr_name: getattr(self, name))) return cls # 测试SQLAlchemy场景(示例) from sqlalchemy import Column, Integer, String from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() @make_readonly_properties class User(Base): __tablename__ = 'users' _id = Column(Integer, primary_key=True) _name = Column(String(50)) _age = Column(Integer) user = User() user._age = 25 print(user.age) # 输出25 user.age = 30 # 报错:属性只读
场景2:属性是实例级别(你的测试代码)
如果要适配实例中动态添加的下划线属性,可以通过给类添加__getattr__和__setattr__方法来拦截属性访问:
def make_readonly_properties(cls): # 保存原有的__getattr__和__setattr__(如果存在) original_getattr = getattr(cls, '__getattr__', None) original_setattr = cls.__setattr__ def __getattr__(self, name): # 先尝试调用原有的__getattr__ if original_getattr is not None: try: return original_getattr(self, name) except AttributeError: pass # 尝试获取对应的下划线开头实例属性 underscore_attr = '_' + name if hasattr(self, underscore_attr): return getattr(self, underscore_attr) raise AttributeError(f"'{cls.__name__}' object has no attribute '{name}'") def __setattr__(self, name, value): # 禁止设置对应下划线属性已存在的只读属性 underscore_attr = '_' + name if hasattr(self, underscore_attr): raise AttributeError(f"'{name}' is a read-only attribute") original_setattr(self, name, value) cls.__getattr__ = __getattr__ cls.__setattr__ = __setattr__ return cls # 测试实例属性场景 @make_readonly_properties class X: def __init__(self): self._age = 34 self._address = 'Unknown street at City Z' self.dog_name = 'Saitama' x = X() print(x.age) # 输出34 print(x.address) # 输出Unknown street at City Z x.age = 18 # 报错:属性只读
PEP规范与双下划线属性适配
相关PEP规范:没有专门针对自动生成只读@property的PEP,但PEP 8有明确约定:
- 单下划线开头的属性(如
_age)是内部属性,建议外部不要直接访问; - 双下划线开头的属性(如
__age)会触发Python的名称修饰(name mangling),自动转换为_类名__属性名,用于避免子类命名冲突。
- 单下划线开头的属性(如
适配双下划线属性:要处理双下划线属性,需要先处理名称修饰后的名字。以类属性为例:
def make_readonly_properties(cls): class_name = cls.__name__ for attr_name in cls.__dict__: # 处理双下划线开头的非特殊属性(排除__init__这类) if attr_name.startswith('__') and not attr_name.endswith('__'): # 提取原始属性名:_X__age -> age raw_name = attr_name[len(f'_{class_name}'):] setattr(cls, raw_name, property(lambda self, name=attr_name: getattr(self, name))) # 处理单下划线开头的属性 elif attr_name.startswith('_') and not attr_name.startswith('__'): prop_name = attr_name[1:] setattr(cls, prop_name, property(lambda self, name=attr_name: getattr(self, name))) return cls @make_readonly_properties class X: __age = 34 # 类属性,被修饰为_X__age x = X() print(x.age) # 输出34
如果是实例中的双下划线属性(如self.__age),实际会被存储为self._X__age,用__getattr__方式处理时,需要先拼接修饰后的名称再判断是否存在。
内容的提问来源于stack exchange,提问作者imbr
相关产品推荐
相关产品推荐

