__slots__ 是最轻量的实例属性写入封禁手段,仅允许声明字段;property 实现读写控制与校验;__setattr__ 提供动态白名单拦截;三者均无法防御 exec/eval 等反射攻击,输入过滤才是根本防线。

用 __slots__ 封死未声明的实例属性写入
Python 默认允许在运行时给任意实例动态添加新属性,这是灵活性的来源,也是安全隐患的入口。如果类设计意图是“只暴露特定字段”,__slots__ 是最轻量、最直接的防御手段。
它不阻止对已声明属性的修改(那是另一层控制),但能立刻拦截对未声明属性的赋值,抛出 AttributeError。
- 在类定义中显式声明
__slots__ = ('name', 'age'),仅允许这两个实例属性存在 - 父类和子类的
__slots__不自动合并;若子类也定义了__slots__,则只继承父类的声明字段(除非子类显式包含父类字段或留空) - 一旦启用
__slots__,实例将不再有__dict__,所以不能用vars(obj)或obj.__dict__.update(...)动态注入 - 注意:
__slots__对类属性(即定义在 class body 中的变量)无效,它只约束实例属性
用 property + 私有命名约定限制属性写入逻辑
当需要更精细的控制——比如允许读取但禁止外部直接赋值,或赋值前需校验——就得靠 @property 和命名约定配合。
Python 没有真正的私有成员,但下划线前缀(如 _value)是强约定,配合 @property 可让外部无法直接写,只能走 setter(或干脆不提供 setter)。
- 定义
@property但不定义对应的@xxx.setter,就实现了“只读”效果 - 若需写入控制,在 setter 中加判断逻辑,例如
if not isinstance(value, int): raise TypeError - 避免用双下划线
__value触发 name mangling——它只是改名防误覆盖,不防反射(getattr(obj, '_ClassName__value')仍可读写) - 注意:
property无法阻止通过object.__setattr__(obj, 'xxx', val)绕过,但这种操作属于明确越界,通常不在“防止外部动态修改”的合理防护范围内
重写 __setattr__ 实现白名单式拦截
如果业务场景要求“只允许修改某几个字段,其余一律拒绝”,且字段名可能动态变化(比如配置驱动),那么重写 __setattr__ 更灵活。
它比 __slots__ 更主动,比 property 更统一,但代价是每次属性赋值都经过一层 Python 函数调用,有轻微性能开销。
- 在
__setattr__中检查name是否在预设白名单内,否则raise AttributeError(f'Cannot set {name}') - 必须用
super().__setattr__(name, value)调用父类实现来真正设置属性,否则会无限递归 - 注意:该方法对所有属性生效,包括
__init__中的初始化赋值,所以白名单得包含构造时必需的字段 - 如果类用了
__slots__,__setattr__依然会被调用,二者可共存,但逻辑要一致,否则容易冲突
警惕 exec、eval 和 types.FunctionType 这类高危反射操作
上面所有手段,都无法防御通过字符串执行代码的方式绕过。比如外部传入一段字符串 "obj._secret = 42",再用 exec() 执行——这已经不属于“修改属性”,而是执行任意代码。
- 任何接受用户输入并传给
exec、eval、compile的接口,本质上都是后门,应彻底禁用或严格沙箱隔离 - 用
types.FunctionType动态构造函数并绑定到实例,也能绕过__slots__和__setattr__,但这需要对方已有执行任意 Python 字节码的能力,属于更高权限场景 - 真正关键的防线不在类内部,而在输入边界:不要让不受信数据进入代码执行流程
防护不是堆砌机制,而是清楚每种手段的边界——__slots__ 防误写,property 防乱改,__setattr__ 防泛滥,而输入过滤才是防恶意的根本。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











