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.radiusc.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 知识体系总览

下面这张图,把本章的核心逻辑串起来了。建议你保存下来,写代码前瞄一眼:

Python 属性体系总览 按归属分类 按访问控制分类 类属性 实例属性 公有 (name) 私有 (__name) 所有实例共享 通过类名修改 每个实例独有 在 __init__ 中定义 外部直接访问 无限制 名字重整 _ClassName__attr ✨ property 装饰器:统一接口,隐藏实现

3.6 总结一下

好,咱们捋一捋今天讲的内容:

  • 类属性 vs 实例属性:类属性大家共享,实例属性各玩各的。别搞混了,尤其是赋值的时候。
  • 属性查找顺序:实例 → 类 → 父类。记住这个顺序,很多诡异的问题就能想通了。
  • 访问控制:单下划线是约定,双下划线是名字重整。我建议团队里统一用单下划线,简单明了。
  • property 装饰器:让你用属性的方式调用方法。适合做校验、计算属性、或者后期想加逻辑又不想改接口的场景。

我个人习惯是:能用 property 解决的,绝不写 getter/setter 方法。代码看起来干净,用起来也舒服。但记住一点——别在 property 里做重活,它终究是个属性,用户不会想到访问一个属性还要等半天。

嗯,今天就到这儿。属性这块看似简单,但用好了,你的代码质量能上一个台阶。


公众号:蓝海资料掘金营,微信deep3321