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. 一图看清区别

对比项实例属性类属性
定义位置__init__ 或实例动态添加类定义顶层
存储位置每个实例各自的内存空间类对象的内存空间
标准访问方式实例.属性类名.属性
同名覆盖优先级最高(先找自己)最低(找不到实例的才找它)
修改影响范围只影响当前实例影响所有实例(只要没被同名覆盖)
典型用途实例特有的数据(姓名、年龄)共享数据(学校、计数器)

8. 三个避坑建议

  1. 尽量别让实例属性和类属性同名——除非你真的在玩“覆盖”这种高级操作,否则只会让代码更难读。
  2. 修改类属性,一定要用类名——实例.属性 = 新值 永远是给实例添加一个新的同名属性。
  3. 动态添加实例属性要谨慎——最好在 __init__@dataclass 里把所有实例属性都显式声明。

看完这篇文章,以后遇到属性访问问题,先想想这张表和“同名覆盖优先级”,基本都能秒定位原因啦 😄