Python 类属性与实例属性详解
初学 Python 面向对象的时候,很多同学都踩过一个坑:明明在类里定义了一个共享的变量,给某个实例赋值之后,其它实例竟然纹丝不动。翻半天文档才搞明白,原来是“同名覆盖”惹的祸。
这背后其实就是实例属性和类属性的访问优先级在起作用。今天这篇文章就用最短的时间,把这两个概念拆开揉碎了讲清楚,顺便送你一份避坑指南。
1. 属性到底绑在谁身上?
在 Python 的面向对象世界里,属性就是挂在对象上的数据。但这里的“对象”有两种:
- 类对象:执行
class Student: 那一刻自动创建
- 实例对象:通过
Student() 调出来的具体学生
这两种对象上都可以挂属性,于是就有了下面这对兄弟:
2. 实例属性:每个人的“私人领地”
实例属性最大的特点就是互不干扰。改一个实例的属性,绝对不会影响另一个实例。
2.1 标准写法:self.xxx 在 __init__ 里
最规范的做法,是在 __init__ 构造方法里通过 self 绑定:
class Student:
def __init__(self, name, age):
self.name = name # 名字归自己
self.age = age # 年龄归自己
创建两个实例,彼此完全独立:
alice = Student("Alice", 18)
bob = Student("Bob", 19)
print(f"Alice: {alice.name} - {alice.age}") # Alice: Alice - 18
print(f"Bob: {bob.name} - {bob.age}") # Bob: Bob - 19
2.2 动态添加也可以,但要克制
Python 允许随时随地给实例“贴”新属性:
alice.score = 95
print(alice.score) # 95
# Bob 没有 score,会报错
# print(bob.score) # AttributeError
这种动态属性虽然灵活,但用在生产代码里很容易把结构搞乱,建议只在 __init__ 里把所有实例属性定义清楚。
3. 类属性:所有实例的“公共仓库”
类属性直接写在类定义的顶层,不需要 self,任何实例(包括还未创建的)都能访问,默认指向同一块内存。
3.1 定义与访问
class Student:
school = "XYZ国际学校" # 类属性
def __init__(self, name):
self.name = name
三种访问方式都能拿到同样的值:
alice = Student("Alice")
bob = Student("Bob")
print(Student.school) # XYZ国际学校
print(alice.school) # XYZ国际学校
print(bob.school) # XYZ国际学校
3.2 什么时候用类属性?
- 存储类级别的常量(比如校训、默认配置)
- 统计信息(比如当前创建了多少个实例)
- 所有实例共享的状态
4. 踩坑重点:同名覆盖优先级
Python 查找属性的顺序是:先找实例自己的属性,找不到再去类里找。这一条规则引出了无数迷惑行为。
4.1 同名属性演示
class Student:
name = "匿名学生"
s = Student()
print(s.name) # 匿名学生 (来自类属性)
print(Student.name) # 匿名学生
给实例加一个同名的 name:
s.name = "小明"
print(s.name) # 小明 (来自实例属性,优先级更高)
print(Student.name) # 匿名学生 (类属性纹丝不动)
删除实例属性后,访问又会回退到类属性:
del s.name
print(s.name) # 匿名学生
⚠️ 这里最坑的一点:用 实例.属性名 = 值 的形式给类属性“赋值”,实际上是在悄悄创建一个同名的实例属性,根本不会改动类那一层。
4.2 正确修改类属性必须用类名
真的想修改所有实例共享的值,必须通过类名操作:
Student.school = "ABC双语学校"
print(alice.school) # ABC双语学校
print(bob.school) # ABC双语学校
5. 经典案例:用类属性统计实例数量
class Student:
total_students = 0 # 类级别的计数器
def __init__(self, name):
self.name = name
Student.total_students += 1 # 必须用类名修改!
测试一下:
print(Student.total_students) # 0
s1 = Student("张三")
print(Student.total_students) # 1
s2 = Student("李四")
print(Student.total_students) # 2
# 尽管实例也能访问到,但尽量一律用类名,更清晰
print(s1.total_students) # 2
注意:如果写成 self.total_students += 1,就会变成给实例添加一个私有计数器,那就彻底白瞎了。
6. 现代写法:@dataclass 怎么处理属性?
从 Python 3.7 开始,@dataclass 装饰器可以帮我们自动生成 __init__、__repr__ 等方法。不过要注意,这里带默认值的字段默认都是实例属性,类属性需要单独定义。
from dataclasses import dataclass
@dataclass
class Student:
name: str
age: int = 18 # 实例属性,带默认值
# 类属性放在这里
total_students: int = 0
school: str = "XYZ国际学校"
def __post_init__(self):
Student.total_students += 1
测试:
s1 = Student("王五")
print(s1) # Student(name='王五', age=18)
print(Student.total_students) # 1
7. 一图看清区别
8. 三个避坑建议
- 尽量别让实例属性和类属性同名——除非你真的在玩“覆盖”这种高级操作,否则只会让代码更难读。
- 修改类属性,一定要用类名——
实例.属性 = 新值 永远是给实例添加一个新的同名属性。
- 动态添加实例属性要谨慎——最好在
__init__ 或 @dataclass 里把所有实例属性都显式声明。
看完这篇文章,以后遇到属性访问问题,先想想这张表和“同名覆盖优先级”,基本都能秒定位原因啦 😄