Python 描述符与属性
目录
学习目标
- 理解
@property装饰器的用法 - 掌握 getter、setter、deleter 的实现
- 理解描述符协议
- 能够使用描述符实现属性验证和计算属性
一、@property 装饰器
@property 允许将方法当作属性来访问,同时可以控制属性的读写行为。
1.1 基本用法
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
"""getter:获取半径"""
return self._radius
@radius.setter
def radius(self, value):
"""setter:设置半径"""
if value < 0:
raise ValueError("半径不能为负数")
self._radius = value
@radius.deleter
def radius(self):
"""deleter:删除半径"""
print("删除半径")
del self._radius
@property
def area(self):
"""计算属性:只读"""
return 3.14159 * self._radius ** 2
@property
def circumference(self):
"""计算属性:只读"""
return 2 * 3.14159 * self._radius
# 使用
c = Circle(5)
print(c.radius) # 5(像属性一样访问)
c.radius = 10 # 像属性一样设置
print(c.area) # 314.159(计算属性)
print(c.circumference) # 62.8318
# c.area = 100 # AttributeError: 没有 setter,只读
# c.radius = -5 # ValueError: 半径不能为负数1.2 属性验证
class Person:
def __init__(self, name, age):
self._name = name
self._age = age
@property
def name(self):
return self._name
@name.setter
def name(self, value):
if not isinstance(value, str) or len(value.strip()) == 0:
raise ValueError("姓名必须是有效的字符串")
self._name = value.strip()
@property
def age(self):
return self._age
@age.setter
def age(self, value):
if not isinstance(value, int):
raise TypeError("年龄必须是整数")
if not 0 <= value <= 150:
raise ValueError("年龄必须在 0-150 之间")
self._age = value
p = Person("Alice", 25)
print(p.name, p.age) # Alice 25
p.name = "Bob"
p.age = 30
print(p.name, p.age) # Bob 30
# p.age = -5 # ValueError
# p.age = 200 # ValueError
# p.name = "" # ValueError二、计算属性
class Rectangle:
def __init__(self, width, height):
self._width = width
self._height = height
@property
def width(self):
return self._width
@width.setter
def width(self, value):
if value <= 0:
raise ValueError("宽度必须为正数")
self._width = value
@property
def height(self):
return self._height
@height.setter
def height(self, value):
if value <= 0:
raise ValueError("高度必须为正数")
self._height = value
@property
def area(self):
"""面积 - 计算属性"""
return self._width * self._height
@property
def perimeter(self):
"""周长 - 计算属性"""
return 2 * (self._width + self._height)
@property
def diagonal(self):
"""对角线 - 计算属性"""
return (self._width ** 2 + self._height ** 2) ** 0.5
r = Rectangle(3, 4)
print(f"面积: {r.area}") # 12
print(f"周长: {r.perimeter}") # 14
print(f"对角线: {r.diagonal:.2f}") # 5.00
r.width = 6
r.height = 8
print(f"新面积: {r.area}") # 48三、描述符协议
描述符是实现了 __get__、__set__ 或 __delete__ 方法的类,用于管理属性的访问。
3.1 描述符基础
class Validator:
"""属性验证描述符"""
def __init__(self, min_value=None, max_value=None):
self.min_value = min_value
self.max_value = max_value
self.name = None
def __set_name__(self, owner, name):
"""设置描述符的名称(Python 3.6+)"""
self.name = name
def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__.get(self.name)
def __set__(self, instance, value):
if self.min_value is not None and value < self.min_value:
raise ValueError(f"{self.name} 必须 >= {self.min_value}")
if self.max_value is not None and value > self.max_value:
raise ValueError(f"{self.name} 必须 <= {self.max_value}")
instance.__dict__[self.name] = value
def __delete__(self, instance):
del instance.__dict__[self.name]
class Person:
age = Validator(min_value=0, max_value=150)
score = Validator(min_value=0, max_value=100)
def __init__(self, name, age, score):
self.name = name
self.age = age
self.score = score
p = Person("Alice", 25, 85)
print(p.age, p.score) # 25 85
p.age = 30
p.score = 95
# p.age = -5 # ValueError: age 必须 >= 0
# p.score = 101 # ValueError: score 必须 <= 1003.2 类型检查描述符
class Typed:
"""类型检查描述符"""
def __init__(self, expected_type):
self.expected_type = expected_type
self.name = None
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__.get(self.name)
def __set__(self, instance, value):
if not isinstance(value, self.expected_type):
raise TypeError(
f"{self.name} 必须是 {self.expected_type.__name__} 类型,"
f"而不是 {type(value).__name__}"
)
instance.__dict__[self.name] = value
class TypedPerson:
name = Typed(str)
age = Typed(int)
height = Typed(float)
def __init__(self, name, age, height):
self.name = name
self.age = age
self.height = height
person = TypedPerson("Alice", 25, 1.75)
print(person.name, person.age, person.height)
# person.age = "25" # TypeError: age 必须是 int 类型
# person.name = 123 # TypeError: name 必须是 str 类型四、使用描述符实现 ORM 风格
class Field:
"""数据库字段描述符"""
def __init__(self, field_type="TEXT", default=None):
self.field_type = field_type
self.default = default
self.name = None
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__.get(self.name, self.default)
def __set__(self, instance, value):
instance.__dict__[self.name] = value
def __repr__(self):
return f"Field({self.field_type}, default={self.default})"
class ModelMeta(type):
"""元类:自动收集字段信息"""
def __new__(mcs, name, bases, namespace):
fields = {}
for key, value in list(namespace.items()):
if isinstance(value, Field):
fields[key] = value
namespace['_fields'] = fields
return super().__new__(mcs, name, bases, namespace)
class Model(metaclass=ModelMeta):
"""ORM 基类"""
def __init__(self, **kwargs):
for name, field in self._fields.items():
value = kwargs.get(name, field.default)
setattr(self, name, value)
def __repr__(self):
attrs = ", ".join(
f"{name}={getattr(self, name)!r}"
for name in self._fields
)
return f"{self.__class__.__name__}({attrs})"
def to_dict(self):
return {name: getattr(self, name) for name in self._fields}
class User(Model):
id = Field("INTEGER", default=0)
name = Field("TEXT", default="")
email = Field("TEXT", default="")
age = Field("INTEGER", default=0)
# 使用
user = User(id=1, name="Alice", email="alice@example.com", age=25)
print(user)
# User(id=1, name='Alice', email='alice@example.com', age=25)
print(user.to_dict())
# {'id': 1, 'name': 'Alice', 'email': 'alice@example.com', 'age': 25}五、@property vs 描述符
| 特性 | @property | 描述符 |
|---|---|---|
| 适用场景 | 单个类的特定属性 | 多个类/多个属性的通用逻辑 |
| 代码复用 | 难以复用 | 可复用描述符类 |
| 复杂度 | 简单 | 较复杂 |
| 灵活性 | 有限 | 高度灵活 |
# @property 适合单个属性
class MyClass:
@property
def value(self):
return self._value
# 描述符适合通用逻辑
class Validator:
def __set__(self, instance, value):
# 验证逻辑...
pass
class ClassA:
x = Validator()
y = Validator()
class ClassB:
z = Validator()六、综合示例
"""
温度类:支持摄氏度和华氏度自动转换
"""
class Temperature:
def __init__(self, celsius=0):
self._celsius = celsius
@property
def celsius(self):
"""摄氏度"""
return self._celsius
@celsius.setter
def celsius(self, value):
self._celsius = value
@property
def fahrenheit(self):
"""华氏度 - 计算属性"""
return self._celsius * 9 / 5 + 32
@fahrenheit.setter
def fahrenheit(self, value):
"""设置华氏度时自动转换为摄氏度"""
self._celsius = (value - 32) * 5 / 9
@property
def kelvin(self):
"""开尔文"""
return self._celsius + 273.15
@kelvin.setter
def kelvin(self, value):
self._celsius = value - 273.15
def __str__(self):
return f"{self.celsius:.1f}°C / {self.fahrenheit:.1f}°F / {self.kelvin:.1f}K"
def __repr__(self):
return f"Temperature(celsius={self.celsius})"
# 使用
t = Temperature(25)
print(t) # 25.0°C / 77.0°F / 298.1K
t.fahrenheit = 100
print(t.celsius) # 37.777...
print(t) # 37.8°C / 100.0°F / 310.9K
t.kelvin = 0
print(t) # -273.1°C / -459.7°F / 0.0K小结
@property将方法转换为属性,支持 getter、setter、deleter- 计算属性是只读的
@property,根据其他属性动态计算 - 描述符是实现
__get__、__set__、__delete__的类 - 描述符适合在多个属性/类之间复用逻辑
@property语法简洁,描述符更灵活可复用- Python 3.6+ 的
__set_name__让描述符使用更方便
练习
- 为
BankAccount类添加balance属性,确保余额不能为负数。 - 实现一个
RangeInt描述符,限制整数属性的取值范围。 - 创建一个
Product类,使用@property实现折扣价自动计算。 - 使用描述符实现一个
NonEmpty验证器,确保字符串属性不为空。 - 实现一个
CachedProperty描述符,只在第一次访问时计算,后续直接返回缓存值。