Python中如何使用__slots__
限制对象属性来节约内存
__slots__
是python中类的一个类属性,它允许我们明确声明类数据对象的成员有哪些,同时取消创建 对象默认自带的 __dict__
和 __weakref__
(除非在__solts__
中也指定要带着这两个属性,或者在父类中提供了他们)。
这对于节约 __dict__
占用的空间来说意义重大,同时属性的查找速度也可以得到显著的提升。
1. 如何声明 __solts__
objecct.__slots__
是一个类变量,我们可以通过使用实例变量名称的字符串、可迭代对象或者字符串序列来为其赋值。推荐使用变量名称的字符串序列,最好使用元组(节约空间)。__slots__
为声明的变量保留空间,并防止为每个实例自动创建__dict__
和 __weakref__
。
如下,我们声明一个User类型:
class User(object):
def __init__(self, id=0, name=None, age=None) -> None:
self.id = id
self.name = name
self.age = age
接下来我们创建一个对象,并查看其所有的属性:
user_1 = User(1, "Jack", 22)
print(dir(user_1))
# ['__class__', '__delattr__', '__dict__', '__dir__',
# '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__',
# '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__',
# '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__',
# '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__',
# '__weakref__', 'age', 'id', 'name']
可见其中存在 __idct__
和 __weakref__
属性。
现在我们使用__slots__
限定该类的自定义属性,并再次查看其属性:
class User(object):
__slots__ = ("id", "name", "age")
def __init__(self, id=0, name=None, age=None) -> None:
self.id = id
self.name = name
self.age = age
user_1 = User(1, "Jack", 22)
print(dir(user_1))
# ['__class__', '__delattr__', '__dir__', '__doc__', '__eq__',
# '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__',
# '__init__', '__init_subclass__', '__le__', '__lt__', '__module__',
# '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__',
# '__setattr__', '__sizeof__', '__slots__', '__str__', '__subclasshook__',
# 'age', 'id', 'name']
现在可以看到 __dict__
和 __weakref__
属性消失了。
不过这同时也意味着,我们不能使用 __dict__()
方法来查看类的自定义属性了(不过可以使用__slots__
了)。
2. __slots__的主要作用:
如果我们确定某个需要大量创建对象的类其属性是固定不变的,不会在运行时动态添加新的属性,那么我们就可以将这个类的属性通过__slots__
“固化”。这样做一方面可以减少程序所占用的内存,另一方面可以加快类中属性的查找速度。
我们通过程序做一个对比,查看使用和不使用__slots__
类属性的类创建大量对象后的内存使用情况:
-
不使用
__slots__
:from tracemalloc import start, stop, take_snapshot class User(object): def __init__(self, id=0, name=None, age=None) -> None: self.id = id self.name = name self.age = age start() users = [] for i in range(1_000_000): users.append(User(i)) snapshot = take_snapshot() used_size = snapshot.statistics('filename')[0] print(f"Memory used with __slots__: {used_size}") stop()
运行结果:
Memory used with __slots__: d:\test.py:0: size=195 MiB, count=3999735, average=51 B
-
使用
__slots__
:from tracemalloc import start, stop, take_snapshot class User(object): __slots__ = ("id", "name", "age") def __init__(self, id=0, name=None, age=None) -> None: self.id = id self.name = name self.age = age start() users = [] for i in range(1_000_000): users.append(User(i)) snapshot = take_snapshot() used_size = snapshot.statistics('filename')[0] print(f"Memory used with __slots__: {used_size}") stop()
运行结果:
Memory used with __slots__: d:\test.py:0: size=96.0 MiB, count=1999746, average=50 B
可以看到使用 __slots__
的程序使用了96M的内存,而未使用的程序占用了195M的内存。可见__slots__
对于节约内存开销是有很大帮助的。
不过,前提是我们定义的类会在程序中被大量创建和使用,对于使用率不高的类,大可不必费此周章限制属性,还是要以易用性为主。
3. 使用 __slots__的注意事项:
-
当从一个没有定义
__slots__
的类继承时,子类的实例总是可以访问__dict__
和__weakref__
属性。 -
没有了
__dict__
变量,实例将不能被分配未在__slots__
定义中列出的新的实例变量。如果尝试访问一个未列出的变量会抛出AttributeError
。如果需要动态分配新的变量,那么可以在__slots__
序列中加入"__dict__"
。 -
如果实例没有了
__weakref__
变量,那么定义__slots__
的类就不支持对其实例的弱引用(弱引用的主要用途是实现保存大对象的高速缓存或映射,但又不希望大对象仅仅因为它出现在高速缓存或映射中而保持存活,不被gc回收销毁。)。如果希望支持弱引用,则可以在__slots__
序列中加入"__weakref__"
。 -
__slots__
在类层面是通过为每个变量名称创建描述符(__get__()
、__set__()
和__delete__()
)来实现的。因此,类属性不能用于设置__slots__
定义的实例变量的默认值;否则,类属性将覆盖描述符分配。 -
对类的
__slots__
声明操作不受限于定义它的类。在父类中定义的__slots__
,子类也可以使用。不过,子类中会得到__dict__
和__weakref__
,除非它们也定义了__slots__
(子类的__slots__
应该只包含额外的slot)。class VipUser(User): def __init__(self, id=0, name=None, age=None, expired=False): super(VipUser, self).__init__(id, name, age) self.expired = expired user = VipUser(1, "Rose", 21, False) print(dir(user)) # ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', # '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', # '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', # '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', # '__setattr__', '__sizeof__', '__slots__', '__str__', '__subclasshook__', # '__weakref__', 'age', 'expired', 'id', 'name']
这里我们创建一个继承自User的之类并查看它的所有属性,可见其又有了
__dict__
和__weakref__
属性。而如果要现在子类的属性,可以只添加其新增的
expired
属性即可:class VipUser(User): __slots__=('expired') def __init__(self, id=0, name=None, age=None, expired=False): super(VipUser, self).__init__(id, name, age) self.expired = expired
-
如果一个类定义了一个也在基类中定义的slot,那么由基类slot定义的实例变量是不可访问的(除非直接从基类中检索其描述符)。这使得程序的含义不明确。将来可能会添加一个检查来防止这种情况。
-
非空
__slots__
不适合用于派生自“可变长度”的内置类型(如int
、bytes
和typle
)的类。 -
任何非字符串迭代对象都可以赋值给
__slots__
。 -
如果将一个字典赋值给了
__slots__
,那么字典的键将会用作 slot 名称。字典的值可以用来提供每个属性的文档字符串,这些文档字符串将被spect.getdoc()
识别,并显示在help()
的输出中。 -
只有当两个类有相同的
__slots__
时,__class__
赋值才有效。 -
在多继承中,子类可以使用父类提供的多个 slots,但只允许一个父类具有 slots 创建的属性(其他基类必须具有空槽布局),违反会引发
TypeError
。 -
如果一个迭代器用于
__slots__
, 那么会为迭代器的每个值创建一个描述符。但是__slots__
属性将会是一个空的迭代器。
参考文档:
https://docs.python.org/3/reference/datamodel.html#object.slots
https://docs.python.org/zh-cn/3/library/weakref.html#module-weakref