在Python的面向对象编程中,每个类的实例默认会有一个__dict__属性,用来动态存储实例的属性和对应的值,这种动态特性虽然灵活,但在需要创建大量实例的场景下会带来额外的内存开销。__slots__是Python提供的一个类属性,通过它可以指定实例允许拥有的属性,从而跳过__dict__的创建,达到优化内存的目的。

__slots__的基本用法
定义类时,在类内部声明__slots__属性,赋值为一个元组或列表,里面的元素是字符串形式的属性名,这些就是实例允许拥有的属性。
class User:
# 定义__slots__,限制实例只能有name和age两个属性
__slots__ = ('name', 'age')
def __init__(self, name, age):
self.name = name
self.age = age
# 创建实例
u = User('张三', 20)
print(u.name) # 输出:张三
print(u.age) # 输出:20
# 尝试添加未定义的属性会报错
u.gender = '男' # 抛出AttributeError: 'User' object has no attribute 'gender'
__slots__优化内存的原理
默认情况下,Python实例的__dict__是一个字典,字典本身有额外的内存开销,而且每个实例的__dict__都是独立存在的。当使用__slots__时,解释器会为类创建一个固定大小的数组来存储实例属性,而不是为每个实例创建__dict__字典,这样就减少了每个实例的内存占用。
我们可以通过sys模块的getsizeof函数来对比两种实例的内存占用差异:
import sys
# 没有使用__slots__的类
class NormalUser:
def __init__(self, name, age):
self.name = name
self.age = age
# 使用__slots__的类
class SlotUser:
__slots__ = ('name', 'age')
def __init__(self, name, age):
self.name = name
self.age = age
# 创建两个实例
normal_u = NormalUser('李四', 25)
slot_u = SlotUser('李四', 25)
# 查看实例本身的内存占用,注意这里不包含属性值的内存
print(f'普通实例内存占用:{sys.getsizeof(normal_u)}字节')
print(f'使用__slots__的实例内存占用:{sys.getsizeof(slot_u)}字节')
# 查看是否有__dict__属性
print(f'普通实例是否有__dict__:{hasattr(normal_u, "__dict__")}') # 输出:True
print(f'使用__slots__的实例是否有__dict__:{hasattr(slot_u, "__dict__")}') # 输出:False
从输出结果可以看到,使用__slots__的实例内存占用明显更低,而且没有__dict__属性,属性直接存储在类预分配的数组中。
__slots__的使用限制
- 使用__slots__的类,实例无法动态添加未在__slots__中定义的属性,这会降低实例的灵活性。
- 如果父类和子类都定义了__slots__,子类的__slots__会继承父类的,同时可以添加自己的属性,但是父类的__slots__不会限制子类的实例属性。
- __slots__不会影响类属性,类属性依然可以正常定义和访问,也不会被__slots__限制。
- 如果类中定义了__slots__,同时想要保留__dict__来允许动态添加属性,可以在__slots__中加入'__dict__',但这样会失去部分内存优化的效果。
适用场景
__slots__适合以下场景:
- 需要创建成千上万个同类型的实例对象,比如数据处理中的大量数据记录对象。
- 实例的属性是固定的,不需要动态添加新属性。
- 对程序的内存占用比较敏感,需要尽可能减少内存消耗。
如果实例数量很少,或者需要频繁动态添加属性,那么使用__slots__的收益不大,反而会降低代码的灵活性,这种情况下不建议使用。