🎩 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)
|
但这样写代码太啰嗦了,每次都要调用方法,不够“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) dog.age = 3
del dog.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.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) m.x = 20 print(m.y)
|
2.2 Property 的本质
猜猜看:property 本身是什么?
print(type(property)) print(property.__bases__)
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 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 print(temp.celsius)
|
3.3 支持装饰器语法
class Circle: def __init__(self, radius): self._radius = radius @MyProperty def radius(self): """圆的半径""" return self._radius @radius.setter 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}") print(f"面积: {circle.area:.2f}") circle.radius = 10 print(f"新面积: {circle.area:.2f}")
|
🎪 第四幕:完整对比与总结
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 如此重要?
- 向后兼容:可以把普通属性改成 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) lst.data.append(2) print(lst.stats)
|
结语
Property 就像是 Python 世界的礼仪教练,它教导属性如何彬彬有礼地待人接物。从简单的 getter/setter 到优雅的装饰器,再到神秘的描述符协议,property 展示了 Python 的哲学:
“简单的事情应该简单,复杂的事情应该可能。”
现在你已经掌握了 property 的魔法,去创造更优雅、更健壮的 Python 代码吧!记住:好的代码就像好的笑话,不需要解释。而 property 就是让代码变得不言自明的秘诀之一。