这是一个非常重要且强大的 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 装饰器,它的语法更简洁,可读性更强。
语法规则:
- Getter: 用
@property装饰get_age方法。 - Setter: 用
@<属性名>.setter装饰set_age方法。 - 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 初学者到进阶开发者的一个重要标志,它让你能够写出既安全又优雅的代码。
