首页 / Python3 入门教程 / 高级特性:__slots__ 与 @property

Python3 入门教程

高级特性:__slots__ 与 @property

本教程共 70 篇 · 第 35 篇 · 更新于 2026-07-22 · 约 5 分钟阅读

PythonPython3 入门教程__slots__@property惰性加载内存优化

35. 高级特性:slots 与 @property

本节目标:深入掌握 __slots__ 限制实例属性的原理,学会用 @property 的完整形态(getter、setter、deleter)控制属性访问。

34 章预告了 __slots__,32 章也初步用过 @property。这一章我们把两个特性挖深一点,看看它们在什么场景下能派上大用场。

slots:给实例属性上锁

默认情况下,Python 实例像个可以自由扩建的仓库。__init__ 里定义了 xy,但你随时能再绑一个 z

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

p = Point(1, 2)
p.z = 3  # 完全没问题

这种灵活性很方便,但代价是每个实例都要维护一个 __dict__ 字典。如果你要创建几百万个实例,内存会吃不消。

slots 限定属性名

class Point:
    __slots__ = ('x', 'y')

    def __init__(self, x, y):
        self.x = x
        self.y = y

p = Point(1, 2)
# p.z = 3  # AttributeError: 'Point' object has no attribute 'z'

__slots__ 是一个元组,列出了实例允许拥有的属性名。不在名单上的属性,绑定就会报错。

Note

用了 __slots__ 后,实例不再拥有 __dict__。这意味着你不能动态添加属性,但也意味着每个实例能省下字典的内存开销。根据官方文档,内存节省可达 50% 以上。

slots 与继承

子类不会自动继承父类的 __slots__。如果子类也定义了 __slots__,它能使用的属性是两者之和:

class Point2D:
    __slots__ = ('x', 'y')

class Point3D(Point2D):
    __slots__ = ('z',)

p = Point3D()
p.x = 1
p.y = 2
p.z = 3
# p.w = 4  # AttributeError

如果子类不定义 __slots__,它会恢复使用 __dict__,父类的 __slots__ 限制仍然有效,但子类自己可以动态添加属性:

class LoosePoint(Point2D):
    pass

lp = LoosePoint()
lp.x = 1
lp.z = 3  # 可以!因为 LoosePoint 有 __dict__
Warning

如果父类有 __slots__ 而子类也有 __slots__,记得给子类的 __slots__ 写逗号:('z',)。单元素元组必须带逗号,否则变成字符串了。

什么时候用 slots

  • 需要创建大量简单对象(比如游戏里的粒子、几何计算中的点)。
  • 属性集合固定,不需要动态扩展。
  • 想明确告诉队友:这个类只有这些属性,别乱加。

如果类的实例数量不多,或者经常需要动态绑属性,别用 __slots__,它会束缚你的手脚。

@property 的完整形态

32 章我们用 @property 做了 getter 和 setter。其实它还有第三个形态:deleter。

回顾 getter 和 setter

class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius

    @property
    def celsius(self):
        return self._celsius

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

t = Temperature(25)
print(t.celsius)   # 25
t.celsius = 30
# t.celsius = -300  # ValueError

加上 deleter

如果你希望在删除属性时做一些清理工作,可以定义 deleter:

class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius

    @property
    def celsius(self):
        return self._celsius

    @celsius.setter
    def celsius(self, value):
        self._celsius = value

    @celsius.deleter
    def celsius(self):
        print("温度数据已清除")
        del self._celsius

t = Temperature(25)
del t.celsius
# t.celsius  # AttributeError

del t.celsius 不是删除 property 本身,而是触发 deleter 里的逻辑。实际应用中 deleter 用得不多,但知道有这个东西,读别人代码时不会懵。

property() 的原始写法

装饰器写法是语法糖。property 本质上是一个内建函数,接受 getter、setter、deleter 和文档字符串:

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

    def get_radius(self):
        return self._radius

    def set_radius(self, value):
        if value <= 0:
            raise ValueError("半径必须大于零")
        self._radius = value

    radius = property(get_radius, set_radius, doc="圆的半径")

c = Circle(5)
print(c.radius)  # 5
c.radius = 10
Circle.radius.__doc__  # '圆的半径'

原始写法在需要动态创建属性时更灵活,但日常开发中 @property 装饰器更简洁。

property 与 slots 的配合

两者可以一起用。如果你用 __slots__ 限定了属性,又想用 property 包装,记得把底层的存储属性也写进 __slots__

class Rectangle:
    __slots__ = ('_width', '_height')

    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 area(self):
        return self._width * self._height

r = Rectangle(3, 4)
print(r.area)   # 12
# r.area = 20   # AttributeError: can't set attribute(没有 setter)

注意 _width_height 带下划线,它们是内部存储。外部通过 r.widthr.area 访问,逻辑清晰,内存也省。

计算属性的惰性加载(进阶技巧)

有时候属性计算很耗时,希望第一次访问时才算,之后直接复用。可以结合 property 和单下划线内部属性实现:

class DataReport:
    def __init__(self, raw_data):
        self.raw_data = raw_data
        self._summary = None

    @property
    def summary(self):
        if self._summary is None:
            print("(正在计算汇总...)")
            self._summary = sum(self.raw_data) / len(self.raw_data)
        return self._summary

report = DataReport([1, 2, 3, 4, 5])
print(report.summary)  # 第一次计算
print(report.summary)  # 直接返回缓存结果

这种模式叫「惰性求值(lazy evaluation)」。数据量大、计算复杂时很管用。

小结

  • __slots__ 限定实例属性,省内存,但牺牲了动态灵活性。
  • @property 让方法像属性一样访问,支持 getter、setter、deleter 三种操作。
  • 两者配合能在性能和封装之间取得平衡。

这两个特性都不算日常开发的高频工具,但遇到性能瓶颈或需要严格接口控制时,它们能帮你写出既优雅又高效的代码。


来源:参考了 liaoxuefeng「使用__slots__」和「使用@property」、pythondoc「9. 类」等,改写后所得。