Python是一门动态语言,这种动态性最直观的体现之一,就是可以在程序运行时给类添加新的属性。正常情况下我们会把属性写在类的定义里,但有些场景下——比如根据配置文件生成属性、在装饰器中为类注入能力、或者编写框架代码时——需要我们在类已经定义好之后再补充属性。这篇文章就来详细聊聊Python中动态添加类属性的几种方法,以及背后的原理。

先弄清楚原理:类属性到底存在哪里
要理解动态添加类属性,得先知道Python中类和实例是如何存储属性的。每个类对象内部都有一个__dict__属性,它本质上是一个映射,存放着这个类定义的所有属性和方法。而实例对象同样有自己的__dict__,存放实例自己的属性。
当我们访问obj.name时,Python的解释器会先在实例的__dict__里找,找不到再去类(以及父类)的__dict__里找。这个查找顺序决定了类属性和实例属性的覆盖关系:实例属性会遮蔽同名的类属性。可以通过下面这段代码直观感受一下:
class Person:
species = 'human' # 类属性
p1 = Person()
p2 = Person()
print(p1.species) # human,实例没有,去类里找
print(Person.species) # human,直接从类访问
p1.species = 'robot' # 这是在实例上创建属性,不是修改类属性
print(p1.species) # robot
print(p2.species) # human,其他实例不受影响
print(Person.species) # human,类属性本身没变
看清楚这一点非常重要:对实例赋值只会影响实例自己的__dict__,而想要动态修改类属性,操作对象必须是类本身,而不是某个实例。这是新手最容易混淆的地方,也是后面所有方法的基础。
方法一:直接对类赋值
最简单直接的方式,就是通过类名点出属性名然后赋值。因为类本身也是对象(type的实例),对它赋值就和给普通变量赋值一样自然:
class Config:
pass
# 动态添加类属性
Config.debug = True
Config.version = '2.3.1'
print(Config.debug) # True
print(Config.version) # 2.3.1
# 所有实例立刻可见
c = Config()
print(c.debug) # True,实例查找时回退到类
这种方式的好处是语法最简洁,可读性好,一眼就能看出在做什么。而且由于实例查找属性时会回退到类,动态添加的类属性对之前已经创建的实例也立即可见,这一点和某些静态语言的编译期绑定完全不同。
需要注意的限制是,属性名必须符合Python标识符的规则,如果属性名来自外部输入(比如配置项名称是log-level这种带横线的字符串),直接赋值就行不通了,得借助下面的方法。
方法二:使用setattr函数
setattr是Python内置函数,专门用来以函数调用的形式设置属性。它接收三个参数:目标对象、属性名字符串和属性值。因为属性名是字符串,所以它可以处理任意动态拼接出来的名称:
class Settings:
pass
# 属性名可以是运行时才知道的字符串
key = 'max_connections'
value = 100
setattr(Settings, key, value)
print(Settings.max_connections) # 100
# 从字典批量注入属性
options = {'host': '127.0.0.1', 'port': 8080, 'retries': 3}
for k, v in options.items():
setattr(Settings, k, v)
print(Settings.host) # 127.0.0.1
print(Settings.port) # 8080
setattr最常见的应用场景就是从字典、配置文件、数据库查询结果中批量生成属性。与之配套的还有getattr和hasattr,分别用于动态获取和检测属性,三者组合起来可以实现非常灵活的属性操作。
有一个细节值得留意:如果类定义了__slots__,那么实例能拥有的属性就被限制在__slots__列出的范围内,此时对实例调用setattr添加未声明的属性会抛出AttributeError。不过这个限制作用于实例,对类本身使用setattr添加类属性依然有效。这在阅读框架源码时经常能碰到。
方法三:操作__dict__和type的进阶玩法
除了上面两种常规手段,还可以直接操作类的__dict__,或者用type在运行时创建全新的类。mappingproxy可以读取,但不能直接修改,所以严格来说不能写Person.__dict__['age'] = 20,但读取它来查看属性结构很常用:
class Animal:
legs = 4
# 查看类属性的真实存储
print(Animal.__dict__['legs']) # 4
# type的三参数形式:动态创建类,同时带上属性
Bird = type('Bird', (Animal,), {'legs': 2, 'fly': True})
print(Bird.legs) # 2,覆盖了父类的值
print(Bird.fly) # True
print(Bird.__mro__) # 可以查看继承链
type的三参数调用形式等价于写一个class语句,第一个参数是类名,第二个是父类元组,第三个是属性字典。ORM框架的模型类大多就是这样在运行时根据字段定义生成的。相比之下,setattr适合给已存在的类打补丁,而type适合从零构建类。
如果需要批量且更底层的控制,还可以借助type.__dict__之外的工具,比如标准库中的types模块或元类编程。但对于绝大多数日常需求,直接赋值加setattr已经足够,没必要为了炫技引入更复杂的方案。
动态添加类属性的常见坑
第一个坑是可变类属性共享问题。如果动态添加的属性是列表、字典这类可变对象,所有实例会共享同一份数据,一个实例通过类修改它,所有实例都受影响:
class Team:
pass
Team.members = []
t1 = Team()
t2 = Team()
t1.members.append('alice')
print(t2.members) # ['alice'],共享了同一个列表
第二个坑是继承体系下的影响范围。给基类动态添加属性时,所有子类及其实例都能访问到这个新属性,这在大型项目中可能引发难以排查的命名冲突。反过来,给子类添加属性不会影响父类。建议在框架开发中给动态属性加上统一前缀,比如_ext_xxx,降低冲突概率。
第三个坑是属性遮蔽。动态添加类属性时,如果某个实例已经有了同名实例属性,实例访问时仍然优先取自己的值,类上的修改对它不可见。排查这类问题时,打印类.__dict__和实例.__dict__对比一下,通常很快就能定位。
总的来说,动态添加类属性是Python灵活性的体现,直接赋值适合固定场景,setattr适合名称来自外部数据的场景,type适合运行时构建完整类。理解了__dict__和属性查找顺序这套底层机制,无论遇到什么属性相关的怪异行为,都能顺着这条线索找到答案。
Python动态添加类属性setattr类属性与实例属性修改时间:2026-09-12 22:08:39