@property 无法阻止赋值,因Python不拦截同名属性赋值;无setter仅防属性访问赋值,不防直接改底层变量;需用__setattr__配合_frozen标记实现真正只读。

为什么直接用 @property 无法阻止赋值?
很多人以为加了 @property 就自动只读,其实不然——Python 不会主动拦截对同名属性的赋值。如果类里同时定义了 self._field 和 @field.setter(哪怕空实现),外部仍能通过 obj.field = ... 触发 setter;而如果压根没写 setter,Python 会抛出 AttributeError: can't set attribute,但这依赖于用户不绕过 property 直接操作底层变量。
真正敏感字段必须防住两类行为:一是通过属性名赋值,二是绕过 property 直接写 _field 或 __field。
- 仅靠
@property+ 无 setter 只能拦第一种,拦不住直接改obj._password - 用双下划线
__password触发名称改写(name mangling),但依然可被obj._User__password访问,不防有意窥探 - 敏感字段需要运行时防护,不是语法糖能解决的
用 __setattr__ 拦截所有赋值尝试
重写 __setattr__ 是最直接的方式:它在每次 obj.attr = val 时被调用,包括初始化过程。关键是要区分“初始化阶段允许设值”和“实例创建后禁止修改”。
典型做法是用一个标记(如 self._frozen)控制开关:
class User:
def __init__(self, password):
super().__setattr__('_password', password)
super().__setattr__('_frozen', True) # 初始化完才冻结
<pre class="brush:php;toolbar:false;">@property
def password(self):
return self._password
def __setattr__(self, name, value):
if getattr(self, '_frozen', False) and name == '_password':
raise AttributeError("password is read-only")
super().__setattr__(name, value)
- 必须用
super().__setattr__绕过自定义逻辑完成初始化,否则__init__会无限递归 - 检查
name == '_password'而非'password',因为 property 的 getter 名是公开接口,实际存储字段是私有变量 - 这个方案对
obj._password = 'new'也生效,比纯@property更严格
配合 __slots__ 防止动态添加敏感字段
如果类启用了 __slots__,Python 就不会生成 __dict__,外部无法新增任意属性(比如 obj._password_backup = ...)。这对敏感字段是加分项,但要注意:
-
__slots__必须显式声明所有允许的实例属性,包括底层存储字段(如_password)和冻结标记(如_frozen) - 如果忘了把
_password加进__slots__,赋值会直接报AttributeError,连初始化都失败 - 继承自内置类型(如
dict、list)时,__slots__可能无效,需实测
补全上面的例子:
class User:
__slots__ = ('_password', '_frozen')
<pre class="brush:php;toolbar:false;">def __init__(self, password):
super().__setattr__('_password', password)
super().__setattr__('_frozen', True)
# ... 其余同上
真正敏感字段建议交给专用库或系统级保护
Python 层面的防护本质是“君子协议”:它能拦误操作,但拦不住有意调试、内存 dump 或子类覆盖 __setattr__。生产环境处理密码、密钥等,应:
- 避免在 Python 对象中长期持有明文敏感值,优先用
secrets模块生成、用cryptography库加密传输/存储 - 考虑用操作系统凭据管理器(如 Linux keyring、Windows CredVault)或专用服务(HashiCorp Vault)托管
- 如果必须用 property 暴露,至少加上日志审计:
logging.warning("password accessed by %s", inspect.stack()[1].function)
靠 @property 做安全边界,就像用纸糊保险柜——看着有层遮挡,但一捅就破。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











