3、属性深度解析:实例属性 vs 类属性,属性的访问控制(公有、私有),`property`装饰器的妙用。
好,咱们今天聊点实在的。属性这个东西,说白了就是对象身上挂的数据。但你别小看它,用好了,代码干净利落;用不好,满屏都是坑。我这些年带团队,光是在属性上翻车的项目就不止一个。今天我把压箱底的经验都掏出来,咱们一次讲透。
3.1 实例属性 vs 类属性:别搞混了
先问个问题:你写了一个类,里面定义的变量,到底是属于这个类的,还是属于对象的?
答案是:看你怎么写的。
核心区别一句话:
- 类属性:定义在类内部、方法外部的变量。所有实例共享一份。
- 实例属性:在
__init__方法里用self.xxx赋值的变量。每个实例独有一份。
来,看代码:
class Student:
# 类属性
school = "蓝海中学"
def __init__(self, name, age):
# 实例属性
self.name = name
self.age = age
# 创建两个学生
s1 = Student("小明", 12)
s2 = Student("小红", 11)
print(s1.school) # 蓝海中学
print(s2.school) # 蓝海中学
print(Student.school) # 蓝海中学
看到了吗?school 这个属性,不管你是通过类访问,还是通过实例访问,结果都一样。这就是类属性——它属于类本身。
我的经验: 类属性适合放那些「所有对象都应该知道」的常量。比如学校名称、公司税率、游戏版本号。我在做一个电商项目时,把商品税率写成类属性,结果后来要分地区收税,改起来那叫一个痛苦。所以记住:会变的配置,别放类属性里。
3.2 属性查找顺序:Python 是怎么找到你的属性的?
你写 s1.school,Python 到底怎么找到这个值的?
它的查找顺序是:实例 → 类 → 父类。
说白了就是:先看看实例自己有没有这个属性,有就用;没有就去类里面找;再没有就去父类找。如果都找不到,就报 AttributeError。
但这里有个坑,我当年踩过:
class Student:
school = "蓝海中学"
s1 = Student()
s2 = Student()
# 给 s1 的实例属性赋值
s1.school = "新东方"
print(s1.school) # 新东方
print(s2.school) # 蓝海中学
print(Student.school) # 蓝海中学
为什么会这样?因为 s1.school = "新东方" 这行代码,不是在修改类属性,而是给 s1 这个实例创建了一个新的实例属性,把类属性给「屏蔽」了。
注意: 如果你真的想修改类属性,必须通过类名来操作:Student.school = "新东方"。通过实例赋值,只会创建实例属性,不会影响类属性。这个坑,我见过太多人掉进去了。
3.3 属性的访问控制:公有 vs 私有
Python 没有真正的「私有」属性。它靠的是约定和名字重整。
咱们先看约定:
| 命名方式 | 含义 | 示例 |
|---|---|---|
name |
公有属性,随便用 | self.name |
_name |
受保护属性,约定「别从外部直接访问」 | self._age |
__name |
私有属性,Python 会做名字重整 | self.__salary |
单下划线开头的,纯粹是君子协定。你非要访问,Python 也不拦你。但团队里大家约定好:看到 _xxx,就当它是内部的,别碰。
双下划线开头的,Python 会偷偷改名字:
class Employee:
def __init__(self, name, salary):
self.name = name
self.__salary = salary
e = Employee("张三", 8000)
print(e.name) # 张三
# print(e.__salary) # 报错!AttributeError
# 但你可以这样访问:
print(e._Employee__salary) # 8000
看到了吧?Python 把 __salary 改成了 _Employee__salary。这不是真正的私有,只是让外部代码不那么容易误访问。
我的建议: 在团队项目里,用单下划线就够了。双下划线容易让代码变得奇怪,而且继承的时候还会出幺蛾子。我一般只在写框架或库的时候才用双下划线,防止用户乱改内部状态。
3.4 property 装饰器:优雅地控制属性访问
好,重点来了。property 装饰器,说白了就是让你用访问属性的方式,调用一个方法。
为什么要这么做?
你想想看,有时候你希望一个属性是「计算出来的」,或者你希望在获取/设置属性时做一些额外操作(比如校验、日志)。用 property 就能做到:
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
"""获取半径"""
return self._radius
@radius.setter
def radius(self, value):
"""设置半径,带校验"""
if value <= 0:
raise ValueError("半径必须大于0")
self._radius = value
@property
def area(self):
"""面积是计算出来的,不需要 setter"""
return 3.14159 * self._radius ** 2
# 使用
c = Circle(5)
print(c.radius) # 5
print(c.area) # 78.53975
c.radius = 10
print(c.area) # 314.159
# c.radius = -1 # 报错!ValueError
你看,c.radius 和 c.area 用起来就像普通属性一样,但背后其实执行了方法。这就是 property 的妙处:对外暴露统一的接口,对内隐藏实现细节。
property 的三种用法:
- @property:定义 getter,让方法像属性一样被读取
- @xxx.setter:定义 setter,让属性赋值时执行校验或转换
- @xxx.deleter:定义 deleter,控制
del obj.xxx的行为
我在项目中遇到过这样一个场景:用户模型里有个 birthday 字段,但业务上经常需要知道年龄。如果每次都去计算,代码会散得到处都是。用 property 就完美解决:
from datetime import date
class User:
def __init__(self, name, birthday):
self.name = name
self._birthday = birthday
@property
def birthday(self):
return self._birthday
@birthday.setter
def birthday(self, value):
if value > date.today():
raise ValueError("生日不能是未来的日期")
self._birthday = value
@property
def age(self):
today = date.today()
return today.year - self._birthday.year - (
(today.month, today.day) < (self._birthday.month, self._birthday.day)
)
u = User("李四", date(1990, 5, 20))
print(u.age) # 自动计算年龄
曾经踩过的坑: 我刚开始用 property 时,喜欢在 getter 里做耗时操作,比如查数据库。结果每次访问属性都卡一下,性能惨不忍睹。记住:getter 应该轻量、快速。如果要做复杂计算,考虑用缓存或者干脆用方法。
3.5 知识体系总览
下面这张图,把本章的核心逻辑串起来了。建议你保存下来,写代码前瞄一眼:
3.6 总结一下
好,咱们捋一捋今天讲的内容:
- 类属性 vs 实例属性:类属性大家共享,实例属性各玩各的。别搞混了,尤其是赋值的时候。
- 属性查找顺序:实例 → 类 → 父类。记住这个顺序,很多诡异的问题就能想通了。
- 访问控制:单下划线是约定,双下划线是名字重整。我建议团队里统一用单下划线,简单明了。
- property 装饰器:让你用属性的方式调用方法。适合做校验、计算属性、或者后期想加逻辑又不想改接口的场景。
我个人习惯是:能用 property 解决的,绝不写 getter/setter 方法。代码看起来干净,用起来也舒服。但记住一点——别在 property 里做重活,它终究是个属性,用户不会想到访问一个属性还要等半天。
嗯,今天就到这儿。属性这块看似简单,但用好了,你的代码质量能上一个台阶。
公众号:蓝海资料掘金营,微信deep3321