Python 类属性和实例属性有什么区别?
简化版
类属性属于类对象,所有实例默认共享;实例属性属于某个实例,只影响当前对象。访问属性时,Python 通常先找实例自己的属性,再找类属性,所以实例属性可以遮蔽同名类属性。
详细版
示例:
class User:
role = "guest" # 类属性
def __init__(self, name):
self.name = name # 实例属性
u1 = User("Tom")
u2 = User("Jerry")
role 定义在类上,u1.role 和 u2.role 默认都能访问到它;name 定义在实例上,每个对象各有一份。
print(u1.role) # guest
print(u2.role) # guest
print(u1.name) # Tom
print(u2.name) # Jerry
如果给实例赋同名属性:
u1.role = "admin"
print(u1.role) # admin
print(u2.role) # guest
print(User.role) # guest
这不是修改类属性,而是在 u1 上新增了实例属性 role,遮蔽了类属性。
高频坑是把可变对象放到类属性里:
class Team:
members = []
t1 = Team()
t2 = Team()
t1.members.append("Tom")
print(t2.members) # ['Tom']
因为 members 是类属性,两个实例共享同一个列表。每个实例独立的数据应该放到 __init__ 里。
完整版教学
一、Python 里的类本身也是对象
在 Python 中,类不是只存在于语法层面的模板,类本身也是对象。
class User:
role = "guest"
执行这段代码时,Python 会创建一个类对象 User,并把 role 放到类对象的命名空间里。
实例化:
u = User()
会创建实例对象。实例对象通常有自己的属性字典,用来保存实例属性。
可以观察:
print(User.__dict__["role"]) # guest
print(u.__dict__) # {}
如果执行:
u.name = "Tom"
实例字典会变成:
{'name': 'Tom'}
这就是类属性和实例属性的存放位置差异。
二、属性查找顺序
当你访问:
u.role
Python 会大致按下面思路查找:
- 先看实例对象上有没有这个属性;
- 如果实例上没有,再去类对象上找;
- 如果类上没有,再沿着继承链找父类;
- 还找不到就抛出
AttributeError。
所以:
u.role = "admin"
会在实例上创建一个新属性。之后再访问 u.role,先命中实例属性,就不会继续读取类属性。
这叫属性遮蔽,不是覆盖类属性。
三、修改类属性和修改实例属性
修改类属性:
User.role = "member"
没有遮蔽同名实例属性的对象,会读到新的类属性:
u1 = User()
u2 = User()
u1.role = "admin"
User.role = "member"
print(u1.role) # admin
print(u2.role) # member
u1 有自己的 role,所以不受类属性变化影响;u2 没有自己的 role,所以继续从类上读取。
四、可变类属性为什么危险
类属性适合放共享常量:
class HttpStatus:
OK = 200
NOT_FOUND = 404
但如果类属性是列表、字典、集合这类可变对象,就很容易变成共享状态。
错误示例:
class Cart:
items = []
def add(self, item):
self.items.append(item)
多个购物车实例会共享一个 items 列表,这通常不是业务期望。
正确写法:
class Cart:
def __init__(self):
self.items = []
def add(self, item):
self.items.append(item)
每次创建实例时都会创建新的列表。
五、类属性适合哪些场景
类属性适合表达“属于整个类”的信息:
- 常量;
- 默认配置;
- 所有实例共享的只读信息;
- 类级计数器或注册表,但要明确管理副作用。
例如:
class User:
count = 0
def __init__(self):
User.count += 1
这里 count 表示创建过多少个用户实例,是类级状态,放在类属性上是合理的。
六、常见误区与追问
记忆钩子:读属性时“先实例、再类、再父类”;写属性时要看写在谁身上。
obj.x = ...通常是在实例上新建或覆盖绑定,Class.x = ...才是在改类属性。
- 误区:实例访问到类属性后,给实例赋值就是修改类属性。
obj.count = 1会在实例__dict__里放一个同名属性,遮蔽类属性;只有Class.count = 1或通过类对象修改才会改变类属性绑定。 - 误区:类属性适合放所有默认值。 不会被原地修改的常量可以放类属性,但列表、字典这类可变对象如果作为共享默认值,很容易让多个实例互相污染。
- 追问:为什么
self.items.append(x)可能影响所有实例? 如果items没有在实例上初始化,而是从类属性里查到同一个列表,那么append改的是共享列表对象,不是给当前实例创建新列表。 - 追问:如何判断属性来自实例还是类? 可以看
obj.__dict__和Class.__dict__;实例字典里有这个名字就是实例属性,否则可能来自类或父类。 - 误区:类属性和静态变量完全等价。 Python 类属性是类对象上的普通属性,也会参与继承和属性查找;子类赋同名属性会遮蔽父类属性,不一定修改父类。
- 追问:类属性适合什么面试场景举例? 适合放常量、所有实例共享的配置、计数器或注册表;但共享可变状态要明确管理,必要时用实例属性或工厂函数创建独立对象。
七、加强记忆
类属性在类上,实例属性在对象上。读属性时先找实例,再找类;写 obj.x = ... 通常是在实例上创建或修改属性,不是在改类属性。可变对象如果要每个实例独立,就放进 __init__;只有真正共享的状态才放类属性。