Python 类属性与实例属性详解
各位 Python 学习者或者刚入门 OOP 的开发者,有没有踩过这种坑:明明在同一个类里定义了一个共享值,给某一个实例赋值后,其他实例居然纹丝不动?最后翻半天文档才发现“同名覆盖”搞的鬼?
别慌,这本质上是对实例属性和类属性的绑定规则、访问优先级没摸透。今天咱们就从基础定义、代码演示、踩坑场景到最佳实践,把这俩属性讲得明明白白~
1. 属性基本概念拆解
在 Python 面向对象编程里,“属性”其实就是绑定到对象上的数据。但这里的“对象”分两种层级:
- 类对象(Class Object):
class Student: 定义完就自动生成的那个
- 实例对象(Instance Object):用
Student() 实例化出来的具体“学生”
对应这两种对象,属性也分成了两类:
2. 实例属性:每个实例的“私人领地”
实例属性是咱们平时写类时接触最多的,它的特点就是独立性强——改一个实例的属性,绝对不会影响到另一个。
2.1 标准定义方式:用 self 在 __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 灵活但需谨慎
Python 是动态语言,实例属性也可以在创建之后随时随地手动加:
# 给 Alice 单独加一个成绩属性
alice.score = 95
print(alice.score) # 输出: 95
# Bob 没有这个属性,直接访问会报错
# print(bob.score) # AttributeError: 'Student' object has no attribute 'score'
不过这种方式尽量少用在生产代码里——代码可读性会变差,还可能引入意外的属性差异。
3. 类属性:所有实例的“公共仓库”
类属性直接在类定义的顶层写,不用加 self,所有实例(包括未来创建的)都能访问,而且默认指向同一个内存地址。
3.1 标准定义与访问方式
类属性既可以用类名访问,也可以用任意实例访问(因为 Python 找属性时会先找实例,找不到就去类里找):
class Student:
# 顶层定义的类属性:所有学生默认属于这个学校
school = "XYZ国际学校"
def __init__(self, name):
self.name = name
# 创建两个实例
alice = Student("Alice")
bob = Student("Bob")
# 三种访问方式都能拿到同一个值
print(f"类名访问:{Student.school}") # 输出: 类名访问:XYZ国际学校
print(f"Alice访问:{alice.school}") # 输出: Alice访问:XYZ国际学校
print(f"Bob访问:{bob.school}") # 输出: Bob访问:XYZ国际学校
3.2 什么时候该用类属性?
类属性主要用来存类级别的共享数据,比如:
- 全局常量(例如学校的固定校训)
- 统计信息(例如当前类创建了多少个实例)
- 实例共享的配置(例如默认的超时时间)
4. 踩坑重灾区:属性访问优先级
刚才提到“Python 找属性先找实例,找不到再找类”,这就是同名覆盖规则——很多初学者的坑就在这里!
4.1 完整演示同名场景
咱们一步步看:
class Student:
# 先定义一个同名的类属性
name = "匿名学生"
# 1. 刚创建实例,自己没有 name 属性,只能访问类的
s = Student()
print(s.name) # 输出: 匿名学生
print(Student.name) # 输出: 匿名学生
# 2. 给实例手动加一个 name 属性!
s.name = "小明"
# 现在实例有自己的了,优先用自己的
print(s.name) # 输出: 小明
# 类属性一点都没变!!!
print(Student.name) # 输出: 匿名学生
# 3. 把实例的 name 属性删掉
del s.name
# 又只能去类里找了
print(s.name) # 输出: 匿名学生
⚠️ 这里有个非常容易误解的点:用实例名给类属性“赋值”,本质上是给实例加了一个同名的私人属性,根本没改到公共仓库!
如果真的想修改所有实例共享的类属性,必须用类名访问并修改:
# 正确修改类属性的方式
Student.school = "ABC双语学校"
# 现在所有实例(包括旧的)都能拿到新值
print(alice.school) # 输出: ABC双语学校
print(bob.school) # 输出: ABC双语学校
5. 实用示例:用类属性统计实例数量
这是类属性最经典的应用场景之一:每次创建实例,就给类的计数器加1。
class Student:
# 公共计数器:初始为0
total_students = 0
def __init__(self, name):
self.name = name
# ⚠️ 必须用类名修改!用 self.total_students +=1 会变成实例的私人属性
Student.total_students += 1
# 测试一下
if __name__ == "__main__":
print(f"初始学生数:{Student.total_students}") # 输出: 0
s1 = Student("张三")
print(f"创建张三后学生数:{Student.total_students}") # 输出: 1
s2 = Student("李四")
print(f"创建李四后学生数:{Student.total_students}") # 输出: 2
# 用实例访问也能拿到类属性(但别这么干,建议统一用类名)
print(f"通过张三访问学生数:{s1.total_students}") # 输出: 2
6. 现代 Python 简化:用 @dataclass 管理属性
从 Python 3.7 开始,标准库新增了 @dataclass 装饰器,可以帮我们自动生成 __init__、__repr__ 等常用方法,管理属性更清晰。
⚠️ 注意:@dataclass 里直接带默认值的字段默认是实例属性,类属性需要单独放在类定义里(或者用 ClassVar 标注,不过简单场景下单独放就行)。
from dataclasses import dataclass
@dataclass
class Student:
# 这里的字段都是实例属性(自动生成 __init__ 绑定)
name: str
# 带默认值的实例属性
age: int = 18
# 类属性单独放在这里
total_students: int = 0
school: str = "XYZ国际学校"
# __post_init__:自动生成的 __init__ 执行完后调用
def __post_init__(self):
Student.total_students += 1
# 测试简化后的类
if __name__ == "__main__":
s1 = Student("王五")
print(s1) # 自动生成的 __repr__,输出: Student(name='王五', age=18)
print(f"学生数:{Student.total_students}") # 输出: 1
7. 核心总结表
最后咱们用一张表把两类属性的核心区别列出来,方便随时查阅:
8. 最后提个醒:避坑小技巧
- 尽量不要让实例属性和类属性同名——除非你刻意想玩“覆盖”这种高级操作,但会让代码可读性骤降。
- 修改类属性必须用类名——别用实例名瞎赋值,不然只会多一个没用的私人属性。
- 动态添加实例属性要克制——生产代码里最好所有属性都在
__init__ 或 @dataclass 里显式定义。
好啦,今天的内容就到这里~以后再遇到属性访问的问题,记得先回忆这张表和“同名覆盖优先级”哦😉