You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用装饰器为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规范与双下划线属性适配

  1. 相关PEP规范:没有专门针对自动生成只读@property的PEP,但PEP 8有明确约定:

    • 单下划线开头的属性(如_age)是内部属性,建议外部不要直接访问;
    • 双下划线开头的属性(如__age)会触发Python的名称修饰(name mangling),自动转换为_类名__属性名,用于避免子类命名冲突。
  2. 适配双下划线属性:要处理双下划线属性,需要先处理名称修饰后的名字。以类属性为例:

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 00:17:40