• 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    接下来我们创建一个对象,并查看其所有的属性:

    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']
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    可见其中存在 __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']
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    现在可以看到 __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()
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18

      运行结果:

      Memory used with __slots__: d:\test.py:0: size=195 MiB, count=3999735, average=51 B
      
      • 1
    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()
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20

      运行结果:

      Memory used with __slots__: d:\test.py:0: size=96.0 MiB, count=1999746, average=50 B
      
      • 1

    可以看到使用 __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']
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14

      这里我们创建一个继承自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
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
    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

  • 相关阅读:
    I2C总线 | I2C总线介绍
    【Linux】Shell命令行的简易实现(C语言实现)内键命令,普通命令
    3.Python_创建型模式_抽象工厂模式
    扑克牌顺子题
    想辞职了,IT部门地位低,在公司天天被业务压制,成了取数机器
    Java-校验规则Integer使用 @NotEmpty注解报错
    9.3.4(数据链路层)
    VM关闭虚拟机之后,连接不上前一天设置的静态ip
    开发知识点-Git
    SSM+Vue+Element-UI实现教资考前指导系统
  • 原文地址:https://blog.csdn.net/weixin_43863487/article/details/126324398