杰瑞科技汇

property函数如何实现属性访问控制?

这是一个非常重要且强大的 Python 内置函数,它允许你将类的方法“伪装”成属性,从而在不改变外部调用方式的情况下,优雅地控制属性的访问和修改。


为什么需要 property()

想象一个简单的场景:我们有一个 Student 类,学生有年龄属性。

class Student:
    def __init__(self, name, age):
        self.name = name
        self.age = age
# 创建一个学生实例
s = Student("Alice", 20)
print(s.age)  # 输出: 20
# Alice 过了一岁生日
s.age = 21
print(s.age)  # 输出: 21

看起来没问题,但如果我们想对年龄做一些限制,比如年龄不能是负数,或者不能超过 150 呢?直接修改 s.age = -10 就会破坏数据的有效性。

为了解决这个问题,我们可能会这样做:

class Student:
    def __init__(self, name, age):
        self.name = name
        self._age = age  # 使用下划线表示这是一个“受保护的”内部变量
    def get_age(self):
        return self._age
    def set_age(self, value):
        if value < 0 or value > 150:
            print("年龄无效!")
            return
        self._age = value
# 创建实例
s = Student("Alice", 20)
# 访问和修改都需要调用方法
print(s.get_age())   # 输出: 20
s.set_age(21)       # 有效
print(s.get_age())   # 输出: 21
s.set_age(-10)      # 输出: 年龄无效!
print(s.get_age())   # 输出: 21 (没有被修改)

虽然这样实现了功能,但代码变得不那么“Pythonic”,我们习惯了通过 obj.attr 的方式访问属性,而不是 obj.get_attr()obj.set_attr()property() 就是为了解决这个问题而生的。


property() 的基本用法

property() 函数可以创建一个属性,它接收四个参数: property(fget, fset, fdel, doc)

  • fget: 获取属性的函数 (getter)。
  • fset: 设置属性的函数 (setter)。
  • fdel: 删除属性的函数 (deleter)。
  • doc: 属性的文档字符串 (docstring)。

让我们用 property() 来重构上面的 Student 类:

class Student:
    def __init__(self, name, age):
        self.name = name
        self.age = age  # 注意这里,我们直接调用了 setter 方法!
    # 1. 定义 getter 方法
    def get_age(self):
        print("正在获取年龄...")
        return self._age
    # 2. 定义 setter 方法
    def set_age(self, value):
        print(f"正在设置年龄为 {value}...")
        if value < 0 or value > 150:
            raise ValueError("年龄必须在 0 到 150 之间!")
        self._age = value
    # 3. 使用 property() 将方法转换为属性
    age = property(get_age, set_age)
# --- 测试 ---
s = Student("Alice", 20)
# 像访问普通属性一样,但实际上调用了 get_age()
print(s.age)  # 输出: 正在获取年龄... \n 20
# 像修改普通属性一样,但实际上调用了 set_age()
s.age = 21   # 输出: 正在设置年龄为 21...
print(s.age)  # 输出: 正在获取年龄... \n 21
# 尝试设置无效年龄
try:
    s.age = -5
except ValueError as e:
    print(e)  # 输出: 年龄必须在 0 到 150 之间!

关键点

  • 我们仍然需要内部的 _age 变量来存储实际值。
  • __init__ 中,我们使用 self.age = age,这会触发 set_age 方法,确保了对象创建时年龄的有效性。
  • 外部代码完全不需要改变,依然使用 s.age 的方式,但背后却有了数据校验的逻辑。

更优雅的 @property 装饰器语法

虽然 property() 函数很好用,但更常见、更推荐的方式是使用 @property 装饰器,它的语法更简洁,可读性更强。

语法规则:

  1. Getter: 用 @property 装饰 get_age 方法。
  2. Setter: 用 @<属性名>.setter 装饰 set_age 方法。
  3. Deleter: 用 @<属性名>.deleter 装饰 del_age 方法。

让我们用装饰器语法重写 Student 类:

class Student:
    def __init__(self, name, age):
        self.name = name
        self.age = age  # 同样,这里会调用 setter
    @property
    def age(self):
        """这是一个年龄属性。"""
        print("正在获取年龄...")
        return self._age
    @age.setter
    def age(self, value):
        print(f"正在设置年龄为 {value}...")
        if value < 0 or value > 150:
            raise ValueError("年龄必须在 0 到 150 之间!")
        self._age = value
    @age.deleter
    def age(self):
        print("正在删除年龄...")
        del self._age
# --- 测试 ---
s = Student("Bob", 25)
# 获取 (调用 getter)
print(s.age) # 输出: 正在获取年龄... \n 25
# 设置 (调用 setter)
s.age = 26   # 输出: 正在设置年龄为 26...
print(s.age) # 输出: 正在获取年龄... \n 26
# 删除 (调用 deleter)
del s.age    # 输出: 正在删除年龄...
print(s._age) # 会报错 AttributeError: 'Student' object has no attribute '_age'

对比两种方式: | 特性 | property(func1, func2, func3) | @property / @age.setter | | :--- | :--- | :--- | | 可读性 | 较差,需要将函数名作为参数传递。 | 极佳,代码结构清晰,一目了然。 | | 函数命名 | get_age, set_age | 都是 age,通过装饰器区分角色。 | | 推荐度 | 可用,但较少使用。 | 强烈推荐,是 Python 社区的标准做法。 |


property 的实际应用场景

property 的应用非常广泛,是 Python 面向对象编程的精髓之一。

数据验证和封装

这是最经典的用途,如上面的年龄例子,确保数据的有效性。

class Product:
    def __init__(self, name, price):
        self.name = name
        self.price = price
    @property
    def price(self):
        return self._price
    @price.setter
    def price(self, value):
        if value < 0:
            raise ValueError("价格不能为负数!")
        self._price = value
p = Product("Laptop", 999)
print(p.price) # 999
p.price = -100 # ValueError: 价格不能为负数!

延迟计算 (Lazy Evaluation)

当一个属性的值计算成本很高,但并非每次都需要时,可以使用 property 进行延迟计算。

class Circle:
    def __init__(self, radius):
        self.radius = radius
        self._area = None  # 初始时未计算
    @property
    def area(self):
        if self._area is None:  # 只有在第一次访问时才计算
            print("正在计算面积...")
            self._area = 3.14159 * (self.radius ** 2)
        return self._area
c = Circle(10)
print(f"半径: {c.radius}")
print(f"第一次访问面积: {c.area}") # 输出: 正在计算面积... \n 314.159
print(f"第二次访问面积: {c.area}") # 直接返回缓存值,不会打印“正在计算...”

维护状态或执行副作用

在设置属性时,除了修改内部变量,还可以执行其他操作,比如触发事件、更新数据库、记录日志等。

class BankAccount:
    def __init__(self, balance):
        self._balance = balance
        self.is_overdrawn = False
    @property
    def balance(self):
        return self._balance
    @balance.setter
    def balance(self, value):
        print(f"余额从 {self._balance} 变为 {value}")
        self._balance = value
        # 检查是否透支
        if self._balance < 0:
            self.is_overdrawn = True
            print("警告:账户已透支!")
        else:
            self.is_overdrawn = False
account = BankAccount(100)
account.balance = 50  # 余额从 100 变为 50
account.balance = -10 # 余额从 50 变为 -10 \n 警告:账户已透支!
print(account.is_overdrawn) # True

特性 描述
核心功能 将方法封装成属性,实现“看似访问属性,实则执行方法”的效果。
主要目的 在不破坏外部接口(obj.attr)的前提下,增加对属性访问的控制逻辑(验证、计算、副作用等)。
两种实现方式 property(fget, fset, fdel) 函数。
@property / @attr.setter / @attr.deleter 装饰器(更推荐)。
关键概念 getter (获取), setter (设置), deleter (删除),通常与一个“私有”的内部变量(如 _attr)配合使用。
应用价值 提高代码的封装性、健壮性和可读性,是编写高质量 Python 类的重要工具。

掌握 property 是从 Python 初学者到进阶开发者的一个重要标志,它让你能够写出既安全又优雅的代码。

分享:
扫描分享到社交APP
上一篇
下一篇