Python类属性与实例属性的区别及访问规则

Owen
Owen
发布时间:2026-11-06| 6人浏览过
原创

详细讲解Python中类属性与实例属性在定义方式、内存存储上的核心区别,梳理解析属性读取的查找顺序、赋值时的遮蔽现象以及可变对象作为类属性的常见陷阱与选型原则。

在Python面向对象编程中,类属性属于类本身,保存在类的命名空间中,由所有实例共享;实例属性属于具体的单个实例,保存在实例各自的命名空间中,彼此相互独立。读取属性时,Python会按照“先查实例自身,再查所属类,最后沿继承链向上查找”的顺序检索;而对属性赋值时,操作目标直接决定了是修改共享数据,还是在当前实例中新建同名属性并遮蔽类属性。

一、定义方式与底层存储位置的区别

区分这两种属性最直接的依据是它们的定义位置与绑定对象。类属性直接写在class代码块内部、所有方法之外,或者在类定义完成后通过类名.属性名 = 值绑定。实例属性则在类的初始化方法__init__或其他实例方法中,通过self.属性名 = 值绑定,也可以在实例创建后通过实例名.属性名 = 值动态添加。

在底层实现中,Python把类和实例都视为独立的对象,并用各自的__dict__字典保存属性(未使用__slots__的情况下)。当Python解释器执行完class语句块时,就会在内存中创建一个类对象,类属性直接存放在这个类对象的__dict__里。每次调用类创建新实例时,Python会为该实例分配一块新的内存空间和独立的__dict__字典,其中只存放属于这个实例自身的属性。

以下代码在Python 3.8及以上环境中均可直接运行。你可以将其保存为attr_basic.py并在终端执行python3 attr_basic.py,观察类与实例各自的命名空间内容:

class Course:
    # 类属性:直接定义在类体中,所有课程实例共享
    platform = "Python在线学堂"
    discount = 0.9

    def __init__(self, title, price):
        # 实例属性:通过 self 绑定到具体实例,每个实例各自独立
        self.title = title
        self.price = price


c1 = Course("数据结构基础", 199)
c2 = Course("网络编程实战", 299)

print("c1.__dict__:", c1.__dict__)
print("c2.__dict__:", c2.__dict__)
print("Course.platform:", Course.platform)
print("platform in Course.__dict__:", "platform" in Course.__dict__)
示例输出:c1.__dict__: {'title': '数据结构基础', 'price': 199} c2.__dict__: {'title': '网络编程实战', 'price': 299} Course.platform: Python在线学堂 platform in Course.__dict__: True

从输出结果可以看到,c1.__dict__和c2.__dict__中仅包含title和price两个键,完全没有platform或discount。类属性只在Course.__dict__中存有一份,不会在每个实例中重复拷贝。

Python类属性与实例属性的区别及访问规则
图1:类属性保存在 Course.__dict__ 中,实例属性保存在各实例独立的 __dict__ 中

二、属性的读取与查找规则

在读取属性时,类只能访问类属性,不能访问实例属性;而实例既可以访问实例属性,也可以访问类属性。之所以存在这一单向兼容性,是因为实例内部维护了指向所属类的指针(即实例.__class__),而类并不绑定某一个特定的实例。

当你对普通属性(不包含自定义@property等数据描述符)执行实例.属性名读取操作时,Python解释器会严格按照以下步骤查找:

  1. 第一步:检查实例自身的__dict__。如果属性名存在于实例.__dict__中,直接返回对应的值,查找立即结束。
  2. 第二步:检查所属类的__dict__。如果实例自身没有该属性,Python会顺着实例.__class__找到所属类,检查类.__dict__中是否存在该属性;若找到则返回。
  3. 第三步:沿父类继承链(MRO)向上查找。如果当前类中也没有,Python会按照方法解析顺序(类.__mro__)依次检查各个父类的__dict__。
  4. 第四步:抛出异常。如果直到顶层基类object仍未找到,也没有定义__getattr__兜底方法,则抛出AttributeError异常。
Python类属性与实例属性的区别及访问规则
图2:执行实例属性读取时的四步查找顺序

通过下面的代码示例,可以验证实例与类在读取属性时的权限差异:

# 实例既能读取实例属性,也能读取类属性
print(c1.title)       # 命中 c1.__dict__['title']
print(c1.platform)    # c1.__dict__ 未命中,回退命中 Course.__dict__['platform']

# 类可以直接读取类属性,但无法读取实例属性
print(Course.platform)
try:
    print(Course.title)
except AttributeError as e:
    print("报错信息:", e)
示例输出:数据结构基础 Python在线学堂 Python在线学堂 报错信息: type object 'Course' has no attribute 'title'

三、属性的修改与“遮蔽”现象(Shadowing)

读取属性时Python会自动向上回退查找,但赋值操作(=)不会向上查找,而是直接写入当前操作对象的__dict__中。这就是初学者最容易混淆的地方:用实例.类属性名 = 新值赋值,并不会修改类属性,而是在该实例自己的__dict__里新建了一个同名实例属性。

一旦实例拥有了与类属性同名的实例属性,后续再通过该实例读取此属性时,按照“先查实例自身”的规则,会直接返回实例__dict__中的值,从而把类属性挡在后面。这种现象称为属性遮蔽(Shadowing)。

# 1. 通过实例给类属性同名名称赋值:只会在 c1.__dict__ 中新建属性
c1.discount = 0.75

print("c1.discount:", c1.discount)          # 读取的是 c1 自身的实例属性
print("c2.discount:", c2.discount)          # c2 没有同名实例属性,仍读取类属性
print("Course.discount:", Course.discount)  # 类属性本身并未改变
print("c1.__dict__:", c1.__dict__)

# 2. 通过类名修改类属性:所有未发生遮蔽的实例都会同步看到新值
Course.discount = 0.85
print("修改类属性后 c2.discount:", c2.discount)
print("修改类属性后 c1.discount:", c1.discount)  # c1 仍被自身实例属性遮蔽

# 3. 删除 c1 的同名实例属性后,遮蔽解除,恢复读取类属性
del c1.discount
print("删除实例属性后 c1.discount:", c1.discount)
示例输出:c1.discount: 0.75 c2.discount: 0.9 Course.discount: 0.9 c1.__dict__: {'title': '数据结构基础', 'price': 199, 'discount': 0.75} 修改类属性后 c2.discount: 0.85 修改类属性后 c1.discount: 0.75 删除实例属性后 c1.discount: 0.85
注意:如果业务逻辑需要将全局折扣从0.9调整为0.85,必须使用Course.discount = 0.85(或在类方法中使用cls.discount = 0.85)。如果在某个实例方法中误写成self.discount = 0.85,只会改变当前单个实例的表现,其他实例不会生效。

四、可变对象作为类属性的常见陷阱

上一节提到通过实例对属性重新赋值(=)不会影响其他实例,但如果类属性是一个可变对象(如列表list、字典dict、集合set),并且通过实例执行了原地修改(如append()、extend()、d[k] = v),结果就会完全不同。

原因在于:执行c1.tags.append("热门")时,Python分两步完成。第一步是读取c1.tags,由于c1.__dict__中没有tags,Python查找到并返回了Course.__dict__['tags']指向的列表对象;第二步是对这个列表对象调用append()方法。整个过程没有发生=赋值,因此不会在c1.__dict__中创建新属性,而是直接修改了所有实例共享的同一个列表。

Python类属性与实例属性的区别及访问规则
图3:重新赋值(=)会新建实例属性,而调用可变对象方法会原地修改共享的类属性

下面的代码对比了错误写法与正确写法。如果每个实例需要维护自己独立的列表或字典,应当将其放入__init__方法中初始化:

class BadCourse:
    # 错误示范:将可变列表作为类属性,期望每个实例有独立标签
    tags = []

    def __init__(self, title):
        self.title = title


class GoodCourse:
    # 正确示范:在 __init__ 中为每个实例创建独立的列表对象
    def __init__(self, title):
        self.title = title
        self.tags = []


b1 = BadCourse("Python入门")
b2 = BadCourse("数据分析")
b1.tags.append("限时免费")
print("BadCourse b2.tags:", b2.tags)  # b2 被意外污染

g1 = GoodCourse("Python入门")
g2 = GoodCourse("数据分析")
g1.tags.append("限时免费")
print("GoodCourse g2.tags:", g2.tags)  # g2 保持独立空列表
示例输出:BadCourse b2.tags: ['限时免费'] GoodCourse g2.tags: []

五、类属性与实例属性的核心对比及选用依据

在实际编写类时,判断一个属性应当定义为类属性还是实例属性,只需看该数据在多个实例之间是否需要保持各自独立的状态。以下表格与总结图梳理了二者的核心差异与日常使用规范:

对比维度 类属性(Class Attribute) 实例属性(Instance Attribute)
定义位置 class块内、方法外部;或通过类名.属性 = 值定义 __init__等实例方法内通过self.属性 = 值定义
存储位置 保存在类.__dict__中,全类仅存一份 保存在实例.__dict__中,每个实例各存一份
读取权限 可通过类名.属性或实例.属性(未被遮蔽时)读取 仅可通过实例.属性读取,通过类名读取会报AttributeError
修改影响范围 通过类名.属性 = 新值修改会影响所有未遮蔽该属性的实例 通过实例.属性 = 新值修改仅影响当前单个实例
典型适用场景 类级别常量、默认配置项、跨实例共享的计数器或缓存池 描述单个对象个体特征的数据(如姓名、价格、独立列表等)
Python类属性与实例属性的区别及访问规则
图4:类属性与实例属性的核心访问机制与编码规范总结

六、特殊限制:定义了__slots__时的行为变化

上述所有基于__dict__的动态添加属性和遮蔽规则,有一个明确的成立前提:类没有定义__slots__,或者__slots__中包含了'__dict__'。如果为了节省内存而在类中声明了__slots__,实例将不再默认生成__dict__字典,此时对同名类属性通过实例赋值会直接报错。

class SlottedCourse:
    __slots__ = ("title", "price")
    discount = 0.9

    def __init__(self, title, price):
        self.title = title
        self.price = price


s = SlottedCourse("算法进阶", 299)
print("读取类属性 s.discount:", s.discount)

try:
    # 尝试通过实例对类属性同名名称赋值,会被 __slots__ 拦截并抛出 AttributeError
    s.discount = 0.8
except AttributeError as e:
    print("赋值报错:", e)
示例输出:读取类属性 s.discount: 0.9 赋值报错: 'SlottedCourse' object attribute 'discount' is read-only

原因在于,当类定义了__slots__且未把discount列入__slots__时,实例没有__dict__可用于新建同名实例属性,因此Python将该类属性在实例层面视为只读属性。在这种情况下,要修改discount只能通过SlottedCourse.discount = 0.8在类对象上操作。

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。