9 minutes
属性装饰器与 property
在 Python 中,属性访问是最常见的操作之一。但简单的属性赋值往往不够——你可能需要校验、计算或延迟加载。@property 装饰器和描述符协议提供了优雅的解决方案,让你能在不破坏外部接口的前提下控制属性行为。
@property 装饰器
从 getter/setter 到 property
我们先看一个传统 getter/setter 的写法:
class Person:
def __init__(self, name: str, age: int):
self._name = name
self._age = age
def get_name(self) -> str:
return self._name
def set_name(self, value: str) -> None:
if not value.strip():
raise ValueError("Name cannot be empty")
self._name = value
p = Person("Alice", 30)
print(p.get_name())
p.set_name("Bob")
这种 Java 风格在 Python 中并不推荐。@property 提供了更 Pythonic 的方式:
class Person:
def __init__(self, name: str, age: int):
self._name = name
self._age = age
@property
def name(self) -> str:
"""获取姓名"""
return self._name
@name.setter
def name(self, value: str) -> None:
if not value.strip():
raise ValueError("姓名不能为空")
self._name = value
@name.deleter
def name(self) -> None:
print(f"删除属性: {self._name}")
del self._name
p = Person("Alice", 30)
# 像普通属性一样访问
print(p.name) # Alice(调用 getter)
# 像普通属性一样赋值
p.name = "Bob" # 调用 setter
print(p.name) # Bob
# p.name = "" # ValueError: 姓名不能为空
# 删除属性
del p.name # 删除属性: Bob
property 的基本语法
class Temperature:
def __init__(self, celsius: float = 0):
self._celsius = celsius
@property
def celsius(self) -> float:
return self._celsius
@celsius.setter
def celsius(self, value: float) -> None:
if value < -273.15:
raise ValueError("温度不能低于绝对零度")
self._celsius = value
# 计算属性:没有 setter,只读
@property
def fahrenheit(self) -> float:
return self._celsius * 9 / 5 + 32
t = Temperature(100)
print(t.celsius) # 100
print(t.fahrenheit) # 212.0
t.celsius = 0
print(t.fahrenheit) # 32.0
# t.fahrenheit = 50 # AttributeError: can't set attribute
Property 的函数式创建
property() 也可以作为函数调用:
class Circle:
def __init__(self, radius: float):
self._radius = radius
def _get_radius(self) -> float:
return self._radius
def _set_radius(self, value: float) -> None:
if value <= 0:
raise ValueError("半径必须为正数")
self._radius = value
def _del_radius(self) -> None:
print("删除半径")
del self._radius
radius = property(_get_radius, _set_radius, _del_radius, "圆的半径")
@property
def area(self) -> float:
return 3.14159 * self._radius ** 2
@property
def diameter(self) -> float:
return self._radius * 2
计算属性
计算属性不存储值,而是在访问时计算:
class Rectangle:
def __init__(self, width: float, height: float):
self.width = width
self.height = height
@property
def area(self) -> float:
return self.width * self.height
@property
def perimeter(self) -> float:
return 2 * (self.width + self.height)
@property
def is_square(self) -> bool:
return abs(self.width - self.height) < 1e-9
r = Rectangle(4, 5)
print(r.area) # 20
print(r.perimeter) # 18
print(r.is_square) # False
r.width = 5
print(r.is_square) # True
带依赖的计算属性
class Order:
def __init__(self, items: list[dict], tax_rate: float = 0.08):
self.items = items
self.tax_rate = tax_rate
@property
def subtotal(self) -> float:
return sum(item["price"] * item.get("qty", 1) for item in self.items)
@property
def tax(self) -> float:
return self.subtotal * self.tax_rate
@property
def total(self) -> float:
return self.subtotal + self.tax
@property
def item_count(self) -> int:
return sum(item.get("qty", 1) for item in self.items)
order = Order([
{"name": "Book", "price": 39.00, "qty": 2},
{"name": "Pen", "price": 5.00, "qty": 3},
])
print(f"小计: {order.subtotal:.2f}") # 小计: 93.00
print(f"税: {order.tax:.2f}") # 税: 7.44
print(f"总计: {order.total:.2f}") # 总计: 100.44
print(f"商品数: {order.item_count}") # 商品数: 5
Property Setter 校验
property 最常见的用途之一是在赋值时进行校验:
import re
class User:
def __init__(self, name: str, email: str, age: int):
self._name = None
self._email = None
self._age = None
# 通过 setter 初始化
self.name = name
self.email = email
self.age = age
@property
def name(self) -> str:
return self._name
@name.setter
def name(self, value: str) -> None:
if not isinstance(value, str):
raise TypeError("姓名必须是字符串")
if not value.strip():
raise ValueError("姓名不能为空")
if len(value) > 50:
raise ValueError("姓名长度不能超过 50 个字符")
self._name = value.strip()
@property
def email(self) -> str:
return self._email
@email.setter
def email(self, value: str) -> None:
if not re.match(r"^[\w.%+-]+@[\w.-]+\.[a-zA-Z]{2,}$", value):
raise ValueError(f"无效的邮箱地址: {value}")
self._email = value.lower()
@property
def age(self) -> int:
return self._age
@age.setter
def age(self, value: int) -> None:
if not isinstance(value, int):
raise TypeError("年龄必须是整数")
if value < 0 or value > 150:
raise ValueError(f"年龄超出有效范围: {value}")
self._age = value
# 构造时自动校验
user = User("Alice Wang", "alice@example.com", 28)
print(user.name) # Alice Wang
print(user.email) # alice@example.com
print(user.age) # 28
# 更新时也会校验
user.age = 29
print(user.age) # 29
# user.age = -1 # ValueError: 年龄超出有效范围: -1
# user.email = "bad" # ValueError: 无效的邮箱地址: bad
缓存属性
@functools.cached_property
对于计算开销大的属性,可以缓存计算结果:
import functools
import time
class DataAnalyzer:
def __init__(self, data: list[int]):
self.data = data
@functools.cached_property
def mean(self) -> float:
"""计算平均值(耗时操作)"""
print("计算平均值...")
time.sleep(1)
return sum(self.data) / len(self.data)
@functools.cached_property
def std(self) -> float:
"""计算标准差"""
print("计算标准差...")
time.sleep(1)
m = self.mean # 使用缓存的 mean
variance = sum((x - m) ** 2 for x in self.data) / len(self.data)
return variance ** 0.5
@functools.cached_property
def summary(self) -> dict:
return {
"count": len(self.data),
"mean": self.mean,
"std": self.std,
"min": min(self.data),
"max": max(self.data),
}
data = DataAnalyzer(list(range(1, 10001)))
# 第一次访问:计算并缓存
start = time.perf_counter()
print(data.mean) # 5000.5(约 1 秒)
print(f"耗时: {time.perf_counter() - start:.2f}s")
# 第二次访问:直接返回缓存值
start = time.perf_counter()
print(data.mean) # 5000.5(瞬间返回)
print(f"耗时: {time.perf_counter() - start:.4f}s")
# 触发 std 计算时复用了缓存的 mean
print(data.std) # 瞬间完成(mean 已缓存)
手动实现可失效的缓存
class WeatherData:
def __init__(self):
self._cache: dict[str, object] = {}
self._cache_time: dict[str, float] = {}
self._ttl = 60 # 缓存 60 秒
def _is_cache_valid(self, key: str) -> bool:
import time
if key not in self._cache_time:
return False
return time.time() - self._cache_time[key] < self._ttl
@property
def temperature(self) -> float:
if self._is_cache_valid("temperature"):
return self._cache["temperature"]
# 模拟 API 调用
import time
time.sleep(0.5)
value = 25.5 # 实际会从 API 获取
self._cache["temperature"] = value
self._cache_time["temperature"] = __import__("time").time()
return value
def invalidate_cache(self) -> None:
self._cache.clear()
self._cache_time.clear()
描述符协议
描述符是 Python 中非常强大的底层机制。property、classmethod、staticmethod 本质上都是描述符。
描述符协议方法
描述符是实现了以下一个或多个方法的类:
__get__(self, obj, objtype)— 获取属性__set__(self, obj, value)— 设置属性__delete__(self, obj)— 删除属性__set_name__(self, owner, name)— Python 3.6+,在类创建时自动调用
class PositiveNumber:
"""描述符:限制值必须为正数"""
def __set_name__(self, owner, name):
"""自动获取属性名"""
self._private_name = f"_{name}"
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self._private_name)
def __set__(self, obj, value):
if not isinstance(value, (int, float)):
raise TypeError("值必须是数字")
if value <= 0:
raise ValueError("值必须为正数")
setattr(obj, self._private_name, value)
class Product:
# 在类级别定义描述符
price = PositiveNumber()
stock = PositiveNumber()
def __init__(self, name: str, price: float, stock: int):
self.name = name
self.price = price
self.stock = stock
p = Product("Laptop", 5999.00, 10)
print(p.price) # 5999.0
print(p.stock) # 10
# p.price = -100 # ValueError: 值必须为正数
# p.stock = "abc" # TypeError: 值必须是数字
带校验的类型描述符
from datetime import datetime
class TypeChecked:
"""类型检查描述符"""
def __init__(self, expected_type):
self.expected_type = expected_type
self._name = None
def __set_name__(self, owner, name):
self._name = f"_typed_{name}"
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self._name, None)
def __set__(self, obj, value):
if not isinstance(value, self.expected_type):
raise TypeError(
f"期望类型 {self.expected_type.__name__}, "
f"实际类型 {type(value).__name__}"
)
setattr(obj, self._name, value)
class Range:
"""范围校验描述符"""
def __init__(self, min_value: float, max_value: float):
self.min_value = min_value
self.max_value = max_value
def __set_name__(self, owner, name):
self._name = f"_range_{name}"
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self._name, None)
def __set__(self, obj, value):
if not self.min_value <= value <= self.max_value:
raise ValueError(
f"{value} 不在范围 [{self.min_value}, {self.max_value}] 内"
)
setattr(obj, self._name, value)
class Student:
name = TypeChecked(str)
age = TypeChecked(int)
score = Range(0, 100)
def __init__(self, name: str, age: int, score: float):
self.name = name
self.age = age
self.score = score
s = Student("Alice", 20, 85.5)
print(s.name, s.age, s.score) # Alice 20 85.5
# s.name = 123 # TypeError: 期望类型 str, 实际类型 int
# s.score = 200 # ValueError: 200 不在范围 [0, 100] 内
内置描述符的工作原理
property 描述符
我们可以手动实现简化版的 property:
class Property:
"""简化版的 property 描述符"""
def __init__(self, fget=None, fset=None, fdel=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.fget is None:
raise AttributeError("unreadable attribute")
return self.fget(obj)
def __set__(self, obj, value):
if self.fset is None:
raise AttributeError("can't set attribute")
self.fset(obj, value)
def __delete__(self, obj):
if self.fdel is None:
raise AttributeError("can't delete attribute")
self.fdel(obj)
def setter(self, fset):
return type(self)(self.fget, fset, self.fdel)
def deleter(self, fdel):
return type(self)(self.fget, self.fset, fdel)
# 使用自定义 Property
class TestClass:
def __init__(self):
self._value = 0
@Property
def value(self):
return self._value
@value.setter
def value(self, new_val):
self._value = new_val
__slots__ 内存优化
默认情况下,每个 Python 对象都有一个 __dict__ 字典来存储实例属性,这有较大的内存开销。__slots__ 可以固定属性集合,减少内存占用:
import sys
class PointWithDict:
"""使用 __dict__ 的类"""
def __init__(self, x: float, y: float):
self.x = x
self.y = y
class PointWithSlots:
"""使用 __slots__ 的类"""
__slots__ = ("x", "y")
def __init__(self, x: float, y: float):
self.x = x
self.y = y
# 内存对比
p1 = PointWithDict(1, 2)
p2 = PointWithSlots(1, 2)
print(f"__dict__ 大小: {sys.getsizeof(p1.__dict__)} bytes") # 约 72 bytes
# __slots__ 的对象没有 __dict__
print(hasattr(p2, "__dict__")) # False
# 大量实例时的内存差异
points_dict = [PointWithDict(i, i) for i in range(100_000)]
points_slots = [PointWithSlots(i, i) for i in range(100_000)]
# 分别计算内存(粗略估计)
import sys
size_dict = sum(sys.getsizeof(p) for p in points_dict[:1000]) / 1000
size_slots = sum(sys.getsizeof(p) for p in points_slots[:1000]) / 1000
print(f"平均每个对象内存 - __dict__: {size_dict:.1f} bytes")
print(f"平均每个对象内存 - __slots__: {size_slots:.1f} bytes")
__slots__ 的注意事项
class SlotsDemo:
__slots__ = ("name", "age")
def __init__(self, name: str, age: int):
self.name = name
self.age = age
s = SlotsDemo("Alice", 30)
print(s.name) # Alice
# s.gender = "F" # AttributeError! 不能添加未在 __slots__ 中声明的属性
# print(s.__dict__) # AttributeError! 没有 __dict__
继承中的 __slots__
class Base:
__slots__ = ("x",)
class Derived(Base):
__slots__ = ("y",) # 也需要声明 __slots__
def __init__(self, x, y):
self.x = x
self.y = y
d = Derived(1, 2)
print(d.x, d.y) # 1 2
# 注意:Derived 仍然没有 __dict__,因为所有父类都有 __slots__
print(hasattr(d, "__dict__")) # False(如果父类没有 __slots__,子类会有 __dict__)
综合示例:数据验证模型
将所学知识综合起来,构建一个简单的验证框架:
class Validator:
"""描述符基类"""
def __set_name__(self, owner, name):
self.name = f"_{name}"
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.name)
def __set__(self, obj, value):
self.validate(value)
setattr(obj, self.name, value)
def validate(self, value):
pass
class NotEmpty(Validator):
def validate(self, value):
if not isinstance(value, str):
raise TypeError("必须是字符串")
if not value.strip():
raise ValueError("不能为空")
class Email(Validator):
import re
def validate(self, value):
if not re.match(r"^[\w.%+-]+@[\w.-]+\.[a-zA-Z]{2,}$", value):
raise ValueError(f"无效的邮箱: {value}")
class Range(Validator):
def __init__(self, min_val=None, max_val=None):
super().__init__()
self.min_val = min_val
self.max_val = max_val
def validate(self, value):
if not isinstance(value, (int, float)):
raise TypeError("必须是数字")
if self.min_val is not None and value < self.min_val:
raise ValueError(f"最小值 {self.min_val},实际 {value}")
if self.max_val is not None and value > self.max_val:
raise ValueError(f"最大值 {self.max_val},实际 {value}")
class Positive(Range):
def __init__(self):
super().__init__(min_val=0)
class Length(Validator):
def __init__(self, min_len=None, max_len=None):
super().__init__()
self.min_len = min_len
self.max_len = max_len
def validate(self, value):
if not isinstance(value, str):
raise TypeError("必须是字符串")
length = len(value)
if self.min_len is not None and length < self.min_len:
raise ValueError(f"最短 {self.min_len} 个字符,当前 {length}")
if self.max_len is not None and length > self.max_len:
raise ValueError(f"最长 {self.max_len} 个字符,当前 {length}")
class ManagedProduct:
"""使用描述符管理的产品类"""
name = NotEmpty()
email = Email()
price = Positive()
quantity = Range(min_val=0, max_val=10000)
description = Length(max_len=500)
def __init__(self, name: str, email: str, price: float, quantity: int, description: str = ""):
self.name = name
self.email = email
self.price = price
self.quantity = quantity
self.description = description
@property
def total_value(self) -> float:
return self.price * self.quantity
# 正确使用
product = ManagedProduct(
name="Python Programming Book",
email="seller@bookstore.com",
price=79.00,
quantity=100,
description="A comprehensive Python book for beginners",
)
print(f"{product.name}: ¥{product.price} x {product.quantity} = ¥{product.total_value}")
# 错误使用
try:
product.price = -50 # ValueError!
except ValueError as e:
print(f"校验失败: {e}")
try:
product.name = "" # ValueError!
except ValueError as e:
print(f"校验失败: {e}")
try:
product.email = "bad-email" # ValueError!
except ValueError as e:
print(f"校验失败: {e}")
常见陷阱
陷阱 1:property 中忘记了 _ 前缀导致递归
class RecursionError:
@property
def name(self):
return self.name # 递归!调用自身
@name.setter
def name(self, value):
self.name = value # 递归!调用自身
class Correct:
@property
def name(self):
return self._name # 正确:访问私有属性
@name.setter
def name(self, value):
self._name = value # 正确
陷阱 2:cached_property 与可变对象
import functools
class Problem:
@functools.cached_property
def items(self) -> list:
return []
obj = Problem()
obj.items.append(1)
obj.items.append(2)
print(obj.items) # [1, 2] —— 缓存后的可变对象可能不是期望的行为
陷阱 3:__slots__ 与 __dict__ 冲突
class OnlySlots:
__slots__ = ("x",)
class Mixed(OnlySlots):
def __init__(self):
self.x = 1
self.y = 2 # 可以!Parent 有 __slots__ 但 Mixed 没有声明,所以它有 __dict__
m = Mixed()
print(m.x, m.y) # 1 2
print(hasattr(m, "__dict__")) # True(Mixed 有 __dict__ 因为没声明 __slots__)
小结
本篇学习了 Python 中属性控制的核心机制:
- @property 装饰器:用 getter/setter/deleter 优雅地控制属性访问
- 计算属性:不存储值,访问时动态计算
- property setter 校验:在赋值时验证数据有效性
- 缓存属性:
@functools.cached_property降低计算开销 - 描述符协议:
__get__、__set__、__delete__、__set_name__构建可复用的属性行为 - property 的描述符本质:理解 property 底层如何工作
__slots__内存优化:固定属性集合,减少内存占用,提升性能
这些机制让你能在不影响外部接口的前提下,精细地控制属性的获取、设置和删除行为。合理使用它们,可以显著提升代码的健壮性和可维护性。
Summary: @property、描述符协议与 slots 内存优化