高级特性:__slots__ 与 @property
本教程共 70 篇 · 第 35 篇 · 更新于 2026-07-22 · 约 5 分钟阅读
35. 高级特性:slots 与 @property
本节目标:深入掌握
__slots__限制实例属性的原理,学会用@property的完整形态(getter、setter、deleter)控制属性访问。
34 章预告了 __slots__,32 章也初步用过 @property。这一章我们把两个特性挖深一点,看看它们在什么场景下能派上大用场。
slots:给实例属性上锁
默认情况下,Python 实例像个可以自由扩建的仓库。__init__ 里定义了 x 和 y,但你随时能再绑一个 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.width 和 r.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. 类」等,改写后所得。