详解Python中的动态属性和特性

发布时间:2026/10/30 5:14:00
详解Python中的动态属性和特性 前言标题里的「特性」指的是property——中文书里常把它译作「特性」或「属性」而「动态属性」指的是运行期才决定名字、才产生的属性。这两件事经常被放在一起讲因为管好动态属性的前提正是先弄清 Python 的属性查找与赋值到底经过哪些钩子。一个典型误解是「Python 对象可以随便加属性所以动态属性就是随便obj.xxx 1。」这只说对了一半。默认的对象确实允许随时新增属性靠实例的__dict__但一旦类里用了__slots__、或者定义了自己的__setattr__/__getattr__/property这套「随便加」的规则就完全变了。搞不清规则就会出现「明明赋了值却读不到」「读属性触发无限递归」这类诡异现象。本文按「属性查找与赋值的四个钩子 →property→ 缓存属性与受限属性 → 描述符式字段」的顺序讲透动态属性并给出可运行的示例。文中所有钩子的行为以 Python 官方数据模型章节为准。本机没有解释器输出为人工推演。一、属性访问的四个钩子Python 把「读、写、删、找不到」拆成四个钩子分工如下钩子何时触发常用场景__getattribute__每次读属性都触发无条件极少数需要全接管属性访问的场合__getattr__常规查找失败、抛AttributeError时才触发代理、懒加载、缺省值__setattr__每次给属性赋值都触发校验、只读属性、代理写__delattr__每次del obj.attr都触发管控删除行为最容易记错的是前两个__getattribute__才是「每次读都走」的那个__getattr__是兜底的、只在找不到时补一脚。官方特别注明只要属性被常规机制找到__getattr__就不会被调用——这种不对称是刻意的既为了效率也为了避免__getattr__没法访问实例的其他属性。二、动态属性的基础__dict__普通对象的属性都存在实例自己的字典里动态增删属性就是往这个字典里加删键# 适用于 Python 3.0class Bag:def __init__(self, **kwargs):self.__dict__.update(kwargs) # 一次性写入b Bag(a1)b.extra 2 # 运行期新增属性print(vars(b)) # {a: 1, extra: 2}del b.extraprint(hasattr(b, extra)) # Falsevars(b)与b.__dict__是同一个东西。类属性也能动态加但类对象的__dict__是只读代理必须走setattr# 适用于 Python 3.0setattr(Bag, kind, container) # ✅ 类属性这样加print(Bag.kind) # container有了这两个工具__getattr__可以做出「点号访问字典键」的代理效果# 适用于 Python 3.0class Config:def __init__(self, data):self._data datadef __getattr__(self, name):try:return self._data[name]except KeyError:raise AttributeError(name) from Nonecfg Config({host: db.local, port: 5432})print(cfg.host) # db.localprint(cfg.missing) # AttributeError: missing注意最后一行找不到键时必须抛AttributeError而不是返回None否则hasattr、getattr的缺省值机制全都会失灵。三、property把属性包装成受控访问property是数据描述符因此它能优先于实例字典成为读数、写数的必经通道。它的构造签名是property(fgetNone, fsetNone, fdelNone, docNone)日常更多用装饰器写法# 适用于 Python 3.0class Temperature:def __init__(self, celsius0):self.celsius celsius # 走 setterpropertydef celsius(self):return self._celsiuscelsius.setterdef celsius(self, value):if value -273.15:raise ValueError(不能低于绝对零度)self._celsius valuet Temperature(25)print(t.celsius) # 25t.celsius 100print(t._celsius) # 100三个装饰器property、x.setter、x.deleter分别设置读取、赋值、删除行为每个都返回一个新的property 对象。只定义 getter 就是只读属性此时给它赋值会抛AttributeError。真实数据存在_celsius里名字带下划线是为了和 property 名区分开。顺带说清版本差异property是数据描述符而 Python 2 的旧式类old-style class不支持描述符协议只有继承object的新式类里property才照上面的规则生效Python 2.7 已于2020 年 1 月 1 日停止维护Python 3 里所有类都是新式类上述行为一致适用不必再区分。四、缓存属性与受限属性缓存属性functools.cached_property在 Python 3.8 引入把方法变成「算一次就缓存」的属性# 适用于 Python 3.8from functools import cached_propertyclass DataSet:def __init__(self, values):self._values list(values)cached_propertydef total(self):print(只计算一次)return sum(self._values)ds DataSet([1, 2, 3])print(ds.total) # 打印只计算一次然后输出 6print(ds.total) # 直接输出 6不再打印它和property的机制不同它只在属性还不存在时运行运行后会把结果写进实例的同名属性之后读取和写入都走普通属性通道。因此它要求实例有可变的__dict__——用了__slots__且没包含__dict__的类用不了它。官方还提示从 Python 3.12 起它去掉了内部锁多线程下 getter 可能被跑不止一次需要同步得自己在 getter 里加锁。受限属性__slots__用来声明固定的一组属性并禁止创建__dict__# 适用于 Python 3.0class Point:__slots__ (x, y)def __init__(self, x, y):self.x xself.y yp Point(1, 2)p.z 3 # AttributeError: Point object has no attribute z一旦用了__slots__「随便动态加属性」就彻底不成立了。这也是记忆点动态加属性的能力来自__dict____slots__的本质是把__dict__去掉。实战描述符式字段与 __set_name__想在类里声明若干「字段」让它们自己知道名字可以用描述符配合__set_name__Python 3.6# 适用于 Python 3.6class Field:def __set_name__(self, owner, name):self.name name # 创建类时自动告知名字def __get__(self, instance, ownerNone):if instance is None:return selfreturn instance.__dict__.get(self.name)def __set__(self, instance, value):instance.__dict__[self.name] valueclass Model:title Field()print(Model.title.name) # titlem Model()m.title helloprint(m.title) # hello__set_name__由type.__new__在类创建时调用参数是所属类和属性名。注意类建好之后再赋值给类属性钩子不会再自动触发要手动调__set_name__。这一步让字段类不必在每次使用时重复写自己的名字是很多 ORM 字段实现的基础。顺带说types.SimpleNamespace是标准库给的一个「随意加属性」的对象适合临时打包数据# 适用于 Python 3.3from types import SimpleNamespacens SimpleNamespace(a1)ns.b 2print(ns) # namespace(a1, b2)常见坑点__getattr__里访问了会再次触发__getattr__的属性❌def __getattr__(self, name): return self.data[name]→ 若data尚未设置读self.data又找不到、又进__getattr__无限递归。 ✅ 用下划线私有名并在__init__里先设好或return object.__getattribute__(self, data)[...]。__getattribute__里用self.xxx取值❌def __getattribute__(self, name): return self.__dict__[name]→ 读self.__dict__又走__getattribute__栈溢出。 ✅ 永远用object.__getattribute__(self, name)取底层值。__setattr__里写self.name value❌def __setattr__(self, name, value): self.__dict__[name] value→ 读self.__dict__又触发__setattr__递归。 ✅ 用super().__setattr__(name, value)或object.__setattr__(self, name, value)。__getattr__找不到时返回None而不抛异常❌def __getattr__(self, name): return self._data.get(name)→ 拼错的属性名也悄悄返回Nonebug 被掩盖。 ✅ 找不到就raise AttributeError(name)让hasattr和缺省值机制正常工作。只写 getter 的 property 被赋值❌property def x(self): ...之后obj.x 1→AttributeError: cant set attribute。 ✅ 需要可写就补x.setter只读是有意为之就别去赋值。给__slots__又定义了同名类变量❌class P: __slots__ (x,); x 1→ 类属性会覆盖槽描述符默认值根本不起作用CPython 上这种定义会直接报错。 ✅ 槽名不要和类变量重名想让槽有默认值在__init__里赋值。对__slots__类用cached_property❌class P: __slots__ (_v,); cached_property def total(self): ...→ 没有可变__dict__装饰器无法把结果写回。 ✅ 把__dict__加进__slots____slots__ (__dict__, ...)或改用property配functools.lru_cache。遍历__dict__时改属性❌for k in vars(obj): setattr(obj, k _old, 1)→RuntimeError: dictionary changed size during iteration。 ✅ 先取快照for k in list(vars(obj)): ...。总结需求用什么关键点随便增删属性实例__dict__/vars()类属性要用setattr类__dict__只读读不到时兜底__getattr__必须抛AttributeError别返回None全接管读属性__getattribute__一律用object.__getattribute__取底层值拦截赋值__setattr__用super().__setattr__别直接self.x 受控读写property数据描述符x.setter/x.deleter真实数据放_x算一次就缓存functools.cached_property3.8需要可变__dict__3.12 起无内置锁固定属性集合__slots__会去掉__dict__动态加属性随之失效动态属性的能力全部来自实例字典property、描述符和__slots__之所以能改变规则的优先级是因为它们要么是数据描述符、要么直接干掉了那个字典。把这条主线抓住四个钩子的触发时机就不会再记混。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询