返回博客

29. __slots__ 与 @property

29. __slots__ 与 @property

第二十九篇:slots 与 @property

前言

Python 类默认允许动态添加属性,这带来了灵活性但也消耗了大量内存。__slots__ 限制了实例属性,节省空间;而 @property 将方法变成属性调用。本文将深入讲解这两大特性及其实际应用。

slots —— 限制实例属性的利器

问题背景

class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email
        self.password = password

u1 = User("小明", "xm@example.com")
u2 = User("小红", "xh@example.com")
print(u1.__dict__)
# {'name': '小明', 'email': 'xiaoming@example.com', 'password': 'secret'}

每个实例都有一个 __dict__ 字典存储属性,开销较大。

使用 slots 节省内存

class UserSlots:
    __slots__ = ["name", "email", "password"]
    
    def __init__(self, name, email, password):
        self.name = name
        self.email = email
        self.password = password

u1 = UserSlots("小明", "xm@example.com", "secret")
u2 = UserSlots("小红", "xh@example.com", "secret")

print(u1.__dict__)  # AttributeError! 没有 __dict__
print(u1.name)      # 小明 — 仍然可以访问

# ⚠️ 不能动态添加新属性
# u1.age = 25  # AttributeError!

slots 的性能对比

import sys

class UserWithDict:
    pass

class UserWithSlots:
    __slots__ = ["name", "age"]

with_dict = [UserWithDict() for _ in range(1_000_000)]
with_slots = [UserWithSlots() for _ in range(1_000_000)]

total_with_dict = sum(sys.getsizeof(vars(u)) for u in with_dict)
total_with_slots = 0  # 无 __dict__ 开销

print(f"有 __dict__: {total_with_dict / 1024**2:.2f} MB")
# 有 __dict__: 38.15 MB
print(f"无 __dict__: {sys.getsizeof(with_slots) / 1024**2:.2f} MB")
# 无 __dict__: ~7.63 MB — 节省约 80% 内存!

slots 的注意事项

class Person:
    __slots__ = ["name", "_age"]
    
    def __init__(self, name, age):
        self.name = name
        self._age = age
    
    @property
    def age(self):
        return self._age
    
    @age.setter
    def age(self, value):
        if not isinstance(value, int) or value < 0:
            raise ValueError("年龄必须是正整数")
        self._age = value

p = Person("小明", 25)
print(p.age)       # 25
p.age = 26         # 合法
p.age = -5         # ValueError!

@property —— 将方法变成属性

基本用法

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    @property
    def area(self):
        """面积(只读属性)"""
        return self.width * self.height
    
    @property
    def perimeter(self):
        return 2 * (self.width + self.height)

rect = Rectangle(10, 5)
print(rect.area)        # 50 — 像属性一样访问
print(rect.perimeter)   # 30

getter / setter / deleter

class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self._balance = balance
    
    @property
    def balance(self):
        """查询余额"""
        return self._balance
    
    @balance.setter
    def balance(self, amount):
        """设置余额(受保护)"""
        if amount < 0:
            raise ValueError("余额不能为负数")
        self._balance = amount
    
    @balance.deleter
    def balance(self):
        """删除余额"""
        del self._balance

account = BankAccount("小明", 1000)
print(account.balance)     # 1000
account.balance = 2000     # 更新余额
print(account.balance)     # 2000
del account.balance        # 删除余额

综合应用示例

from datetime import datetime

class Task:
    def __init__(self, title, priority="normal"):
        self.title = title
        self._priority = priority
        self._created_at = datetime.now()
        self.completed = False
    
    @property
    def priority(self):
        return self._priority
    
    @priority.setter
    def priority(self, value):
        valid_values = ["low", "normal", "high"]
        if value not in valid_values:
            raise ValueError(f"优先级必须是: {', '.join(valid_values)}")
        self._priority = value
    
    @property
    def created_at(self):
        return self._created_at.strftime("%Y-%m-%d %H:%M:%S")
    
    def complete(self):
        self.completed = True

task = Task("完成报告", "high")
print(task.priority)          # high
print(task.created_at)        # 2026-09-09 12:00:00
print(task.completed)         # False
task.complete()
print(task.completed)         # True
task.priority = "invalid"     # ValueError!

实战:数据验证模型

class DataValidator:
    """数据验证基类"""
    
    def __init__(self, **kwargs):
        for key, value in kwargs.items():
            if hasattr(type(self), key):
                setattr(self, key, value)  # 触发 property setter
            else:
                raise AttributeError(f"未知字段:{key}")
    
    @classmethod
    def from_dict(cls, data: dict):
        instance = cls()
        for key, value in data.items():
            setattr(instance, key, value)
        return instance


class UserModel(DataValidator):
    __slots__ = ["name", "age", "email", "phone"]
    
    def __init__(self):
        self.name = None
        self.age = None
        self.email = None
        self.phone = None
    
    @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) or not (0 <= value <= 150):
            raise ValueError("年龄必须在 0~150 之间")
        self._age = value
    
    @property
    def email(self):
        return self._email
    
    @email.setter
    def email(self, value):
        if "@" not in value or "." not in value:
            raise ValueError("邮箱格式错误")
        self._email = value
    
    @property
    def phone(self):
        return self._phone
    
    @phone.setter
    def phone(self, value):
        digits = "".join(c for c in value if c.isdigit())
        if len(digits) != 11 or not digits.startswith("1"):
            raise ValueError("手机号格式错误")
        self._phone = digits


# 使用示例
try:
    user = UserModel(name="小明", age=25, email="xm@test.com", phone="13800138000")
    print(user.name)
    print(user.age)
    print(user.email)
    print(user.phone)
except ValueError as e:
    print(f"错误:{e}")

# 批量处理
data_list = [
    {"name": "张三", "age": 20, "email": "zhangsan@test.com", "phone": "13800138000"},
    {"name": "", "age": -5, "email": "invalid", "phone": "123"},  # 错误数据
]

valid_users = []
for data in data_list:
    try:
        user = UserModel.from_dict(data)
        valid_users.append(user)
    except ValueError as e:
        print(f"跳过:{data['name']} — {e}")

总结

本文讲解了 slots 和 @property 的核心知识:

  1. slots — 限制实例属性,节省大量内存,禁用动态添加
  2. @property getter — 将方法变成只读属性
  3. @property setter — 添加验证逻辑,保证数据合法性
  4. @property deleter — 控制属性删除行为
  5. 性能优化 — 百万级对象可节省 80%+ 内存
  6. 最佳实践 — 私有变量前缀 _ + property 封装

下一篇文章将深入讲解多重继承与 Mixin 模式。


本文是 Python 教程系列的第二十九篇。