Python中如何使用__slots__限制对象属性来节约内存

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__类属性的类创建大量对象后的内存使用情况:

  1. 不使用__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
    
  2. 使用__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__的注意事项:

  1. 当从一个没有定义__slots__ 的类继承时,子类的实例总是可以访问 __dict____weakref__ 属性。

  2. 没有了 __dict__ 变量,实例将不能被分配未在__slots__定义中列出的新的实例变量。如果尝试访问一个未列出的变量会抛出 AttributeError。如果需要动态分配新的变量,那么可以在__slots__ 序列中加入 "__dict__"

  3. 如果实例没有了 __weakref__ 变量,那么定义 __slots__ 的类就不支持对其实例的弱引用(弱引用的主要用途是实现保存大对象的高速缓存或映射,但又不希望大对象仅仅因为它出现在高速缓存或映射中而保持存活,不被gc回收销毁。)。如果希望支持弱引用,则可以在__slots__序列中加入 "__weakref__"

  4. __slots__ 在类层面是通过为每个变量名称创建描述符(__get__()__set__()__delete__())来实现的。因此,类属性不能用于设置 __slots__ 定义的实例变量的默认值;否则,类属性将覆盖描述符分配。

  5. 对类的__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
    
  6. 如果一个类定义了一个也在基类中定义的slot,那么由基类slot定义的实例变量是不可访问的(除非直接从基类中检索其描述符)。这使得程序的含义不明确。将来可能会添加一个检查来防止这种情况。

  7. 非空 __slots__ 不适合用于派生自“可变长度”的内置类型(如 intbytestyple)的类。

  8. 任何非字符串迭代对象都可以赋值给 __slots__

  9. 如果将一个字典赋值给了 __slots__ ,那么字典的键将会用作 slot 名称。字典的值可以用来提供每个属性的文档字符串,这些文档字符串将被 spect.getdoc() 识别,并显示在 help() 的输出中。

  10. 只有当两个类有相同的__slots__ 时,__class__赋值才有效。

  11. 在多继承中,子类可以使用父类提供的多个 slots,但只允许一个父类具有 slots 创建的属性(其他基类必须具有空槽布局),违反会引发TypeError

  12. 如果一个迭代器用于 __slots__, 那么会为迭代器的每个值创建一个描述符。但是 __slots__ 属性将会是一个空的迭代器。


参考文档:

https://docs.python.org/3/reference/datamodel.html#object.slots

https://docs.python.org/zh-cn/3/library/weakref.html#module-weakref

https://www.osgeo.cn/cpython/library/tracemalloc.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值