Python 类属性与实例属性:从原理到实战

目录

一、两种属性的区别

二、属性读取演示

完整演示代码

三、类属性全局修改

四、实例赋值的坑

五、属性查找规则

六、完整流程示例

七、del 删除属性

八、实战:统计实例数量

九、动态添加类属性

十、两类属性对比

十一、易错清单

十二、本章小结


上一章我们学会了基础的类、实例、__init__构造方法与实例属性,我们知道self.xxx是每一个对象独有的数据:每只小猫有自己的名字、毛色,每台手机有专属品牌、售价,互相不会干扰。但现实里还有一类数据,属于同一类事物全体共用,不需要每个实例单独存一份。比如所有手机都属于“智能手机”这个大类,这个分类标签不用苹果、小米各自存一遍,放在模板里统一管理就足够。

今天我们就来拆解两种属性的核心区别:实例属性(对象独有)类属性(全类共享),理清读取、修改的全部规则,避开极易踩坑的同名赋值陷阱。

一、两种属性的区别

我们以手机产品模板来举例,直观看懂两种数据的定位:

class Phone:
    # 类属性:全品类手机共用分类标签
    category = "智能手机"
    def __init__(self, brand, price):
        # 实例属性:单台手机独有的信息
        self.brand = brand
        self.price = price

1. 类属性 category

相当于产品大类统一标识,所有手机共用这一条内容,只保存一份。如果修改这个分类名称,所有手机读取到的标签都会同步更新。

2. 实例属性 brand、price

是单台设备专属信息,华为、小米各自的品牌、价格互不干扰,修改其中一台不会影响其他设备。

简单总结:

  • 实例属性 = 每个对象私有信息,彼此隔离;
  • 类属性 = 全类共享信息,一处修改,全部对象同步生效。

二、属性读取演示

完整演示代码

class Phone(object):
    # 定义类属性,放在所有函数外部
    category = "智能手机"
    def __init__(self, brand, price):
    # self开头为实例属性,每个实例独立
        self.brand = brand
        self.price = price

新建两台手机实例
p1 = Phone("苹果", 5999)
p2 = Phone("小米", 3999)

读取实例专属数据,两台结果不一样
print(p1.brand)
print(p2.brand)
print(p1.price)
print(p2.price)

读取共享类属性,两种写法都可以
写法1:直接使用类名读取,规范推荐
print(Phone.category)
写法2:通过实例读取,结果完全相同
print(p1.category)
print(p2.category)

运行输出结果:

苹果
小米
5999
3999
智能手机
智能手机
智能手机

能看出,不管通过类还是任意实例,拿到的类属性数值完全一致。

三、类属性全局修改

如果使用「类名.属性名」修改类属性,所有创建好的实例再读取时,都会拿到更新后的内容。下面给出完整可运行的代码,包含完整的 Phone 类定义:

class Phone:
    # 类属性:全品类手机共用分类标签
    category = "智能手机"
    def __init__(self, brand, price):
    # 实例属性:单台手机独有的信息
        self.brand = brand
        self.price = price

p1 = Phone("苹果", 5999)
p2 = Phone("小米", 3999)
通过类统一修改共享分类
Phone.category = "5G智能手机"
两台设备读取到的内容同步变化
print(p1.category)
print(p2.category)
print(Phone.category)

输出内容统一变为5G智能手机,实现全局统一修改。

四、实例赋值的坑

很多初学者会直接用实例修改类属性名称,这种操作不会改动全局共享数据,只会给当前实例新增一份同名私有数据,造成数据隔离。沿用第二节的 Phone 类,只贴改动片段:

p1 = Phone("苹果", 5999)
p2 = Phone("小米", 3999)
# 错误操作:通过实例给 category 赋值
p1.category = "老年机"
# p1 只会读取自己新增的私有数据
print(p1.category)
# 类和其他实例不受任何影响
print(Phone.category)
print(p2.category)

运行输出:

老年机
智能手机
智能手机

底层逻辑:实例读取数据时,会优先查找自身携带的属性,只有自身不存在对应字段时,才会读取类里的共享数据。一旦给实例新增同名属性,全局共享内容就不再对它生效。

五、属性查找规则

为什么实例赋值会「隔离」出私有数据?答案藏在 Python 的属性查找顺序里。这是理解两种属性差异的核心难点,务必掌握。

属性查找的完整底层规则:Python实例读取属性时,会按照固定顺序逐层查找:

  1. 先在当前实例自身查找该属性,找到就直接使用;
  2. 实例自身不存在该属性,再去所属类中查找类属性;
  3. 类中也没有,就抛出 AttributeError 报错。

配套演示报错代码:

class Phone:
    category = "手机"
p = Phone("小米")
print(p.price)  # 实例、类都无 price,直接报错

运行这段代码会抛出AttributeError: 'Phone' object has no attribute 'price',因为实例p自身没有price,类Phone里也没有price,两层都找不到,最终只能报错。

六、完整流程示例

配套简短代码,完整走完定义、读取、修改、隔离全过程。沿用第二节的 Phone 类,只贴改动片段:

p1 = Phone("apple")
p2 = Phone("xiaomi")
# 初始共享标签
print(Phone.category)
print(p1.category)
print(p2.category)
# 类统一修改标签
Phone.category = "手机"
print(p1.category)
print(Phone.category)
print(p2.category)
# 实例创建独立同名属性
p1.category = "5G手机"
print(p1.category)
print(Phone.category)
print(p2.category)

七、del 删除属性

除了读取和修改,属性还可以用 del 关键字删除。删除实例独有属性和删除类全局属性,效果完全不同,下面分别演示

1. 删除实例独有属性

实例属性是每个对象单独持有的数据,用 del 实例.属性名 可以把它从当前实例上移除,其他实例不受影响:

class Phone:
    def __init__(self, brand, price):
        self.brand = brand
        self.price = price
p1 = Phone("苹果", 5999)
p2 = Phone("小米", 3999)
# 删除 p1 的实例属性 price
del p1.price
# p1 再读取 price 会报错
print(p1.price)  # AttributeError
# p2 的 price 不受影响
print(p2.price)  # 3999

2. 删除类全局属性

类属性属于整个类共享,用 del 类名.属性名 删除后,所有实例再读取该属性都会报错:

class Phone:
    category = "智能手机"
p1 = Phone("苹果")
p2 = Phone("小米")
# 删除类属性 category
del Phone.category
# 所有实例再读取都会报错
print(p1.category)  # AttributeError
print(p2.category)  # AttributeError

3. 对比:del 实例同名属性后,会重新读取类属性

结合前面学过的查找规则,如果实例自身有同名属性,删除它之后,实例会重新回到类里读取共享的类属性:

class Phone:
    category = "智能手机"
p1 = Phone("苹果")
# 给实例新增同名属性,覆盖类属性
p1.category = "老年机"
print(p1.category)  # 老年机(读取实例自身)
# 删除实例自带的 category
del p1.category
# 实例自身没有了,重新读取类属性
print(p1.category)  # 智能手机

原因解释:del p1.category 只删除实例自身新增的同名属性,类里共享的 category 仍然存在。删除后实例再读取时,自身找不到该属性,就会按查找顺序回到类中读取,所以输出恢复为 智能手机

八、实战:统计实例数量

类属性最常用的场景是全局统计,记录一共创建了多少个实例,不需要为每一个对象单独记录数字。沿用第二节的 Phone 类,只贴改动片段:

class Phone:
    # 类属性:全局计数器,所有实例共用
    count = 0
    category = "智能手机"
    def __init__(self, brand):
    # 实例属性:单台手机独有的信息
        self.brand = brand
    # 每创建一台设备,总数 +1
        Phone.count += 1
连续创建三台设备
p1 = Phone("华为")
p2 = Phone("vivo")
p3 = Phone("oppo")
读取全局统计数量
print("已生产设备总数量:", Phone.count)
print("设备统一分类:", Phone.category)

运行结果:

已生产设备总数量: 3
设备统一分类:智能手机

反面错误案例:不要用 self.count += 1 做全局计数

很多新手会写成 self.count += 1,以为这样就能实现全局计数,其实这是错误写法,只会给当前实例新增一份独立的 count,全局共享的 count 永远保持初始值:

class Phone:
    count = 0
    def __init__(self, brand):
        self.brand = brand
        self.count += 1  # 错误写法!只会给实例新增 count,全局 count 永远为 0
p1 = Phone("华为")
print(Phone.count)  # 输出 0,计数失效

原因解释:self.count += 1 等价于先读取再赋值,由于实例自身没有 count,读取时拿到的是类里的共享值 0,但赋值时却是在当前实例上新建了一个同名属性 count,并不会修改类共享的 count。所以全局计数必须写成 Phone.count += 1,直接通过类名修改类属性,才能让所有实例同步看到更新。

九、动态添加类属性

前面看到的类属性都是写在 class 代码块内部,其实类属性并不强制要求定义在类里。代码任意位置,只要通过「类名.属性名」赋值,就能给类动态新增一个类属性,所有实例同样可以读取到:

class Phone:
    pass
类外部动态添加类属性
Phone.count = 0
p1 = Phone()
p2 = Phone()
所有实例都能读取到动态新增的类属性
print(Phone.count)  # 0
print(p1.count)     # 0
print(p2.count)     # 0

运行结果:

0
0
0

原因解释:类属性本质上就是挂在类对象上的一个名字,只要通过 类名.属性名 = 值 赋值,无论写在类定义内部还是外部,效果完全一样。这也说明类属性不局限于写在类定义里,代码任意位置通过类名都能新增,进一步拓展了对类属性的认知。

业务场景举例:程序运行中动态新增分类标签。比如电商后台在运营过程中新增了一个「限时秒杀」分类,不需要修改类定义代码,直接通过 Goods.category = "限时秒杀" 动态挂到类上,所有已创建的商品实例立刻都能读取到这个新标签,无需逐个对象赋值。

十、两类属性对比

到这里,实例属性和类属性的核心差异已经全部讲完。下面用一张对比表,把两者的定义位置、存储方式、读取方式、修改影响和删除效果一次性梳理清楚,方便复习和查阅。

对比维度实例属性类属性
定义位置__init__ 方法内,通过 self.属性名 定义在类代码块内、所有方法外部,直接 属性名 = 值 定义
存储方式每个实例单独保存一份,互不干扰整个类只保存一份,所有实例共享
读取方式实例.属性名类名.属性名实例.属性名 均可
通过类修改不适用(类无法直接修改实例属性)类名.属性名 = 新值,所有实例同步更新
通过实例赋值正常修改当前实例自己的数据不会修改类属性,只会给当前实例新增同名私有属性
删除方式del 实例.属性名,只影响当前实例del 类名.属性名,所有实例再读取都会报错
典型应用每台手机的品牌、价格等专属信息全品类统一分类标签、全局计数器等共享数据

一句话记忆:实例属性管「个体差异」,类属性管「群体共性」。写代码时先想清楚这份数据是每个对象各自持有,还是全体对象共用,再决定用哪种属性,就能避免绝大多数混淆。

十一、易错清单

本章涉及多个容易踩坑的细节,下面把高频易错点集中列出来,配合正确写法一起对照

易错点 1:用实例赋值想修改类属性

错误写法:p1.category = "老年机",以为能改全局分类。

正确认知:这只会给 p1 新增一份同名私有属性,类和其他实例完全不受影响。要修改类属性,必须用 Phone.category = "老年机"

易错点 2:用 self.count += 1 做全局计数

错误写法:在 __init__ 里写 self.count += 1,以为能统计实例总数。

正确写法:必须写成 Phone.count += 1,直接通过类名修改类属性,才能让所有实例共享同一个计数器。

易错点 3:忘记属性查找顺序

实例读取属性时,永远先找自身,再找类。一旦实例有了同名属性,类里的共享值就被「屏蔽」了。删除实例同名属性后,又会重新读取到类属性。

易错点 4:删除类属性导致全局报错

del Phone.category 会删除整个类共享的属性,之后所有实例再读取 category 都会抛出 AttributeError。删除类属性前务必确认没有其他地方还在使用它。

易错点 5:混淆「读取」和「赋值」的路径

读取时 p1.categoryPhone.category 结果一样,但赋值时两者效果完全不同:前者新增实例属性,后者修改类属性。读取和赋值要分开记忆,不能混为一谈。

十二、本章小结

本章围绕实例属性和类属性,系统梳理了从定义、读取、修改到删除的完整规则,核心要点可以概括为以下几条:

  • 实例属性是每个对象独有的数据,通过 self.属性名 定义,彼此隔离,互不影响。
  • 类属性是全类共享的数据,定义在类代码块内、方法外部,一处修改,所有实例同步生效。
  • 读取规则:实例读取属性时,先找自身,再找类,两层都没有就抛出 AttributeError
  • 修改规则:通过类名修改类属性才能全局生效;通过实例赋值只会新增同名私有属性,不会改动类属性。
  • 删除规则del 实例.属性名 只影响当前实例;del 类名.属性名 影响所有实例。
  • 实用场景:类属性非常适合做全局计数器、统一分类标签等共享数据;实例属性适合保存每个对象的专属信息。

掌握好「实例管个体、类管群体」这条主线,再配合属性查找顺序,就能在实战中准确判断该用哪种属性,写出更清晰、更不易出错的 Python 代码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值