Python 实例属性和类属性:个人储物柜与班级公告栏
引言:两种储物方式
想象你走进一家公司:
- 个人储物柜:每个人一个柜子,里面放自己的私人物品——你的水杯、我的充电器、他的外套。柜子里的东西互不干扰。
- 公司公告栏:贴在走廊墙上,所有人看到的是同一张公告。行政改一次,全公司都看到新内容。
Python 的类也有这两种"储物方式":
- 实例属性:属于每个具体的实例(对象),互不干扰;
- 类属性:属于类本身,所有实例共享。
搞混这两种属性,是初学者最容易踩的坑之一。
一、实例属性:每个实例的私人储物柜
1.1 通过 self 绑定
class Student(object):
def __init__(self, name):
self.name = name # 实例属性,存在每个实例自己身上
s = Student('Bob')
print(s.name) # Bobself.name = name 把数据绑定到当前这个实例上。每个实例独立:
s1 = Student('Alice')
s2 = Student('Bob')
print(s1.name) # Alice
print(s2.name) # Bob改一个不影响另一个:
s1.name = 'Charlie'
print(s1.name) # Charlie
print(s2.name) # Bob(没变)1.2 通过实例变量动态绑定
Python 是动态语言,创建后还可以随时"贴标签":
s = Student('Bob')
s.score = 90 # 动态绑定新属性
print(s.score) # 90这个 score 只存在于 s 这个实例上,其他实例没有:
s2 = Student('Alice')
print(s2.score) # AttributeError: 'Student' object has no attribute 'score'生活化理解:self.name 是出厂时就装好的储物柜;s.score 是你后来自己挂的小袋子——只有你这间柜子有。
二、类属性:全班的公共公告栏
2.1 在类中直接定义
class Student(object):
name = 'Student' # 类属性,属于类本身注意:没有 self.,直接写在类体里。
2.2 实例和类都能访问
>>> s = Student()
>>> print(s.name) # Student(实例没找到,去类里找)
>>> print(Student.name) # Student(直接访问类属性)实例访问 s.name 时,Python 的查找顺序:
- 先看实例自己有没有
name; - 没有?去类里找;
- 类里也没有?报错
AttributeError。
2.3 类属性的典型用途:统计实例数量
class Student(object):
count = 0 # 类属性:统计创建了多少个学生
def __init__(self, name):
self.name = name
Student.count += 1 # 每创建一个实例,计数+1测试:
>>> print(Student.count) # 0
>>> bart = Student('Bart')
>>> print(Student.count) # 1
>>> lisa = Student('Lisa')
>>> print(Student.count) # 2所有实例共享同一个 count——bart 和 lisa 操作的是同一个数字。
生活化理解:公告栏上写着"本班已报到 2 人",每个学生来报到,行政就在公告栏上改数字。所有人看到的是同一张公告。
三、关键陷阱:同名属性会"屏蔽"
3.1 实例属性屏蔽类属性
class Student(object):
name = 'Student' # 类属性
s = Student()
>>> print(s.name) # Student(访问类属性)
>>> s.name = 'Michael' # 给实例绑定同名属性
>>> print(s.name) # Michael(实例属性屏蔽了类属性!)
>>> print(Student.name) # Student(类属性还在,只是被挡住了)3.2 删除实例属性后,类属性"重现"
>>> del s.name # 删除实例的 name
>>> print(s.name) # Student(类属性又出现了)3.3 为什么这是"陷阱"?
class Student(object):
count = 0
def __init__(self, name):
self.name = name
# ❌ 错误:self.count += 1 创建的是实例属性!
# 它屏蔽了类属性 count,类里的 count 永远是 0
# 正确写法:
# Student.count += 1如果你不小心写成 self.count += 1,每个实例都会创建一个自己的 count(值是 1),而类的 count 始终是 0——统计完全失效。
生活化理解:公告栏上写着"已报到 0 人",你自己柜子里贴了一张"已报到 1 人"。别人看公告栏还是 0,你看自己柜子以为报到了——信息不对称,全乱套。
四、实例属性 vs 类属性:一张表看懂
| 对比项 | 实例属性 | 类属性 |
|---|---|---|
| 定义位置 | __init__ 里 self.xxx | 类体中直接写 xxx = ... |
| 归属 | 每个实例独立拥有 | 类本身,所有实例共享 |
| 访问方式 | 实例.xxx | 类名.xxx 或 实例.xxx |
| 修改影响 | 只影响当前实例 | 影响所有实例 |
| 内存 | 每个实例一份 | 只有一份 |
| 典型用途 | 实例的个性化数据(姓名、成绩) | 共享配置、计数器、常量 |
五、常见误区与避坑指南
5.1 误区一:用 self.xxx 修改类属性
class Student(object):
count = 0
def __init__(self, name):
self.name = name
self.count += 1 # ❌ 创建实例属性,屏蔽类属性
s1 = Student('A')
s2 = Student('B')
print(Student.count) # 0(没变化!)
print(s1.count) # 1(实例自己的)
print(s2.count) # 1(实例自己的)修正:修改类属性必须用 类名.xxx:
def __init__(self, name):
self.name = name
Student.count += 1 # ✅ 操作类属性5.2 误区二:在类属性中放可变对象
class Student(object):
scores = [] # ❌ 危险!所有实例共享同一个列表
def __init__(self, name):
self.name = name
s1 = Student('A')
s2 = Student('B')
s1.scores.append(90)
print(s2.scores) # [90] —— s2 也"看到"了!所有实例共享同一个列表,改一个全变。
修正:可变对象放 __init__ 里作为实例属性:
def __init__(self, name):
self.name = name
self.scores = [] # ✅ 每个实例独立5.3 误区三:以为 del 实例属性 会删除类属性
class Student(object):
name = 'Student'
s = Student()
s.name = 'Michael'
del s.name
print(s.name) # Student(类属性还在!)
print(Student.name) # Studentdel s.name 只删实例属性,类属性纹丝不动。想删类属性用 del Student.name。
5.4 误区四:把类属性当"全局变量"滥用
class Config(object):
debug = True
timeout = 30
# 到处改
Config.debug = False类属性确实可以当全局配置用,但缺乏封装、容易被意外修改。建议:
- 用
@property提供只读访问; - 或改用模块级常量(
config.py里写DEBUG = True)。
六、实战练习
练习 1:实例计数器
完成下面的 Student 类,每创建一个实例,count 自动加 1:
class Student(object):
count = 0
def __init__(self, name):
self.name = name
# 你的代码
# 测试
if Student.count != 0:
print('测试失败!')
else:
bart = Student('Bart')
if Student.count != 1:
print('测试失败!')
else:
lisa = Student('Lisa')
if Student.count != 2:
print('测试失败!')
else:
print('Students:', Student.count)
print('测试通过!')参考答案
class Student(object):
count = 0
def __init__(self, name):
self.name = name
Student.count += 1 # 关键:用类名操作,不是 self练习 2:共享配置与独立数据
设计一个 Car 类:
- 类属性
wheels = 4(所有汽车默认 4 轮) - 实例属性
brand和speed - 方法
show_info()打印品牌和车轮数
c1 = Car('BMW')
c2 = Car('Audi')
c1.show_info() # BMW: 4 wheels
c2.show_info() # Audi: 4 wheels
Car.wheels = 6 # 改成 6 轮(概念车)
c1.show_info() # BMW: 6 wheels
c2.show_info() # Audi: 6 wheels参考答案
class Car(object):
wheels = 4 # 类属性:所有车共享
def __init__(self, brand):
self.brand = brand # 实例属性:各自独立
self.speed = 0
def show_info(self):
print(f'{self.brand}: {Car.wheels} wheels')注意 Car.wheels = 6 修改后,所有实例都看到新值——这就是类属性的共享特性。
练习 3:找出 bug
下面代码想统计创建了多少个实例,但结果始终是 0,找出原因:
class Student(object):
count = 0
def __init__(self, name):
self.name = name
self.count += 1
s1 = Student('A')
s2 = Student('B')
print(Student.count) # 期望 2,实际 0参考答案
self.count += 1 创建的是实例属性 count,它屏蔽了类属性。
执行过程:
self.count先读取——实例没有,找到类属性count = 0;self.count = 0 + 1 = 1——给实例创建了一个count = 1;- 类属性
Student.count始终是 0。
修复:
def __init__(self, name):
self.name = name
Student.count += 1 # 明确操作类属性七、小结
- 实例属性通过
self.xxx绑定,属于各个实例,互不干扰——像个人储物柜; - 类属性在类体中直接定义,属于类本身,所有实例共享——像班级公告栏;
- 访问顺序:实例属性优先,找不到才找类属性——同名会屏蔽;
- 修改类属性必须用
类名.xxx,self.xxx会创建实例属性导致屏蔽; - 类属性别放可变对象(如列表),否则所有实例共享同一个,改一个全变;
- 典型用途:实例属性存个性化数据,类属性存共享配置和计数器。
分清"个人储物柜"和"班级公告栏",你的类设计就会清晰、安全、不出怪事。