🎩 Python 的 Property:从“绅士淑女”到“幕后导演”的优雅变身术

引言:当属性开始“装腔作势”

在 Python 的世界里,类属性本可以直接暴露,像穿着睡衣在家一样随意:

class Dog:
def __init__(self):
self.age = 2 # 直接公开,毫无隐私!

dog = Dog()
dog.age = -5 # 哎呀,狗狗的年龄怎么能是负数?

这就像把银行密码写在额头上一样危险!于是我们发明了 getter 和 setter:

class Dog:
def __init__(self):
self._age = 2

def get_age(self):
return self._age

def set_age(self, value):
if value < 0:
raise ValueError("年龄不能为负!")
self._age = value

dog = Dog()
dog.set_age(-5) # ValueError: 年龄不能为负!

但这样写代码太啰嗦了,每次都要调用方法,不够“Pythonic”。这时,property 闪亮登场!

🎯 第一幕:Property 的优雅舞步

1.1 内置 property() 函数:属性界的“变形金刚”

class Dog:
def __init__(self):
self._age = 2

def get_age(self):
print("有人读取年龄啦!")
return self._age

def set_age(self, value):
print(f"有人想把年龄改成 {value}!")
if value < 0:
raise ValueError("年龄不能为负!")
self._age = value

def del_age(self):
print("警告:年龄属性被删除了!")
del self._age

# 看好了,魔法在这里!
age = property(get_age, set_age, del_age, "这是狗狗的年龄属性")

# 使用起来美滋滋
dog = Dog()
print(dog.age) # 自动调用 get_age
dog.age = 3 # 自动调用 set_age
# dog.age = -1 # 报错!
del dog.age # 自动调用 del_age

property() 函数的参数:

  • fget: 获取属性时调用的函数
  • fset: 设置属性时调用的函数
  • fdel: 删除属性时调用的函数
  • doc: 属性的文档字符串

1.2 @property 装饰器:语法糖的甜蜜暴击

Python 觉得上面还是不够优雅,于是推出了装饰器版本:

class Dog:
def __init__(self):
self._age = 2
self._name = "旺财"

@property
def age(self):
"""狗狗的年龄(只读)"""
return self._age

@property
def name(self):
"""狗狗的名字"""
return self._name

@name.setter # 注意这里的写法!
def name(self, value):
if not value:
raise ValueError("名字不能为空!")
self._name = value

@name.deleter
def name(self):
raise AttributeError("名字不能删除!")

dog = Dog()
print(dog.age) # 可以读取
# dog.age = 5 # 报错!age是只读的
dog.name = "小黑" # 可以设置
# del dog.name # 报错!

🎭 第二幕:揭开描述符的神秘面纱

2.1 什么是描述符?

描述符就是一个实现了 __get____set____delete__ 其中一个或多个方法的类。它是 Python 属性访问的幕后导演

class RevealAccess:
"""一个简单的描述符,记录所有访问"""

def __init__(self, initval=None, name='var'):
self.val = initval
self.name = name

def __get__(self, obj, objtype):
print(f"获取 {self.name}: {self.val}")
return self.val

def __set__(self, obj, val):
print(f"设置 {self.name}{val}")
self.val = val

class MyClass:
x = RevealAccess(10, '变量 "x"')
y = 5 # 普通属性,不是描述符

m = MyClass()
print(m.x) # 触发 __get__
m.x = 20 # 触发 __set__
print(m.y) # 直接访问,不触发描述符

2.2 Property 的本质

猜猜看:property 本身是什么?

print(type(property))  # <class 'type'>
print(property.__bases__) # (<class 'object'>,)

# 惊不惊喜?property 就是一个实现了描述符协议的类!
class PropertyDescriptor:
"""property 的简化版实现"""

def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
self.__doc__ = doc

def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.fget is None:
raise AttributeError("不可读")
return self.fget(obj)

def __set__(self, obj, value):
if self.fset is None:
raise AttributeError("不可写")
self.fset(obj, value)

def __delete__(self, obj):
if self.fdel is None:
raise AttributeError("不可删除")
self.fdel(obj)

def setter(self, fset):
self.fset = fset
return self

def deleter(self, fdel):
self.fdel = fdel
return self

🔨 第三幕:亲手打造自己的 Property

3.1 自定义 MyProperty 类

class MyProperty:
"""自制 property,实现 90% 的 property 功能"""

def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
if doc is None and fget is not None:
doc = fget.__doc__
self.__doc__ = doc
self._name = ''

def __set_name__(self, owner, name):
self._name = name

def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.fget is None:
raise AttributeError(f"属性 '{self._name}' 不可读")
return self.fget(obj)

def __set__(self, obj, value):
if self.fget is not None and self.fset is None:
raise AttributeError(f"属性 '{self._name}' 只读")
if self.fset is None:
raise AttributeError(f"属性 '{self._name}' 不可写")
self.fset(obj, value)

def __delete__(self, obj):
if self.fdel is None:
raise AttributeError(f"属性 '{self._name}' 不可删除")
self.fdel(obj)

def getter(self, fget):
"""创建新的 MyProperty,继承现有的 setter 和 deleter"""
return type(self)(fget, self.fset, self.fdel, self.__doc__)

def setter(self, fset):
"""创建新的 MyProperty,继承现有的 getter 和 deleter"""
return type(self)(self.fget, fset, self.fdel, self.__doc__)

def deleter(self, fdel):
"""创建新的 MyProperty,继承现有的 getter 和 setter"""
return type(self)(self.fget, self.fset, fdel, self.__doc__)

3.2 使用我们的 MyProperty

class Temperature:
def __init__(self):
self._celsius = 0

def get_celsius(self):
print("获取摄氏度")
return self._celsius

def set_celsius(self, value):
print(f"设置摄氏度: {value}")
self._celsius = value

# 使用我们自制的 MyProperty!
celsius = MyProperty(get_celsius, set_celsius)

@celsius.setter # 这个也能用!
def celsius(self, value):
if value < -273.15:
raise ValueError("温度不能低于绝对零度!")
self._celsius = value

temp = Temperature()
temp.celsius = 25 # 设置摄氏度: 25
print(temp.celsius) # 获取摄氏度 \n 25
# temp.celsius = -300 # ValueError!

3.3 支持装饰器语法

class Circle:
def __init__(self, radius):
self._radius = radius

@MyProperty
def radius(self):
"""圆的半径"""
return self._radius

@radius.setter # 我们的 MyProperty 也支持!
def radius(self, value):
if value < 0:
raise ValueError("半径不能为负")
self._radius = value

@MyProperty
def area(self):
"""圆的面积(只读)"""
return 3.14159 * self._radius ** 2

circle = Circle(5)
print(f"半径: {circle.radius}") # 5
print(f"面积: {circle.area:.2f}") # 78.54
circle.radius = 10
print(f"新面积: {circle.area:.2f}") # 314.16
# circle.area = 100 # AttributeError: 属性 'area' 只读

🎪 第四幕:完整对比与总结

4.1 三种属性访问方式对比

方式 优点 缺点 适用场景
直接访问 简单快速 无法控制访问 简单数据结构
Getter/Setter 完全控制 代码冗长 Java 风格代码
Property 优雅可控 需要理解原理 Pythonic 封装

4.2 Property 的工作流程图

访问 obj.attr 时:
┌─────────────────────────────────────────┐
│ 1. 查找 obj.__class__.__dict__['attr'] │
│ ↓ │
│ 2. 如果找到的是描述符(有 __get__) │
│ ↓ │
│ 3. 调用 descr.__get__(obj, type(obj)) │
│ ↓ │
│ 4. 返回结果 │
└─────────────────────────────────────────┘

设置 obj.attr = value 时:
┌─────────────────────────────────────────┐
│ 1. 查找 obj.__class__.__dict__['attr'] │
│ ↓ │
│ 2. 如果找到的是描述符(有 __set__) │
│ ↓ │
│ 3. 调用 descr.__set__(obj, value) │
│ ↓ │
│ 4. 完成设置 │
└─────────────────────────────────────────┘

4.3 最佳实践小贴士

class Person:
"""Property 使用最佳实践示例"""

def __init__(self, first_name, last_name):
self._first_name = first_name
self._last_name = last_name
self._age = 0

@property
def first_name(self):
"""名 - 基本不变,只读即可"""
return self._first_name

@property
def last_name(self):
"""姓 - 基本不变,只读即可"""
return self._last_name

@property
def full_name(self):
"""全名 - 由其他属性计算得出,只读"""
return f"{self._first_name} {self._last_name}"

@property
def age(self):
"""年龄 - 需要验证"""
return self._age

@age.setter
def age(self, value):
if not isinstance(value, int):
raise TypeError("年龄必须是整数")
if value < 0 or value > 150:
raise ValueError("年龄必须在 0-150 之间")
self._age = value

@property
def is_adult(self):
"""是否成年 - 计算属性,只读"""
return self._age >= 18

🎬 大结局:为什么 Property 如此重要?

  1. 向后兼容:可以把普通属性改成 property,不影响现有代码
  2. 数据验证:设置属性时自动检查数据有效性
  3. 计算属性:动态计算值,而不是存储值
  4. 惰性求值:需要时才计算,节省资源
  5. 访问控制:实现只读、只写、不可删除等权限
  6. 调试追踪:记录所有属性访问,方便调试
# 最后来个炫技版的 property 应用
class SmartList:
"""一个会自动统计的列表"""

def __init__(self):
self._data = []
self._access_count = 0

@property
def data(self):
self._access_count += 1
print(f"第 {self._access_count} 次访问列表")
return self._data

@property
def stats(self):
return {
"长度": len(self._data),
"访问次数": self._access_count,
"平均长度": self._access_count / len(self._data) if self._data else 0
}

lst = SmartList()
lst.data.append(1) # 第 1 次访问列表
lst.data.append(2) # 第 2 次访问列表
print(lst.stats) # {'长度': 2, '访问次数': 2, '平均长度': 1.0}

结语

Property 就像是 Python 世界的礼仪教练,它教导属性如何彬彬有礼地待人接物。从简单的 getter/setter 到优雅的装饰器,再到神秘的描述符协议,property 展示了 Python 的哲学:

“简单的事情应该简单,复杂的事情应该可能。”

现在你已经掌握了 property 的魔法,去创造更优雅、更健壮的 Python 代码吧!记住:好的代码就像好的笑话,不需要解释。而 property 就是让代码变得不言自明的秘诀之一。