【问题标题】:How to dynamically change __slots__ attribute?如何动态更改 __slots__ 属性?
【发布时间】:2015-03-10 12:53:30
【问题描述】:

假设我有一个__slots__ 的课程

class A:
    __slots__ = ['x']

a = A()
a.x = 1   # works fine
a.y = 1   # AttributeError (as expected)

现在我要更改 __slots__A

A.__slots__.append('y')
print(A.__slots__)   # ['x', 'y']
b = A()
b.x = 1   # OK
b.y = 1   # AttributeError (why?)

b 是在 __slots__A 更改之后创建的,因此 Python 原则上可以为 b.y 分配内存。为什么没有?

如何正确修改类的__slots__,使新实例具有修改后的属性?

【问题讨论】:

    标签: python python-3.x slots


    【解决方案1】:

    在我看来a type turns __slots__ into a tuple as one of it's first orders of action。然后是stores the tuple on the extended type object。由于在这一切之下,python 正在查看tuple,因此无法对其进行变异。事实上,我什至不确定你是否可以访问它,除非你首先将一个元组传递给实例。

    您设置的原始对象仍然作为类型的属性保留这一事实(也许)只是为了方便自省。

    不能修改 __slots__ 并期望它显示在某个地方(实际上——从可读性的角度来看,你可能不真的想要无论如何都要这样做,对吗?)...

    当然,你总是可以继承子类来扩展槽:

    >>> class C(A):
    ...   __slots__ = ['z']
    ... 
    >>> c = C()
    >>> c.x = 1
    >>> c.z = 1
    

    【讨论】:

      【解决方案2】:

      您不能在创建类后动态更改__slots__ 属性,不。这是因为该值用于为每个插槽创建特殊的descriptors。来自__slots__ documentation

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

      可以在__dict__类中看到描述符:

      >>> class A:
      ...     __slots__ = ['x']
      ... 
      >>> A.__dict__
      mappingproxy({'__module__': '__main__', '__doc__': None, 'x': <member 'x' of 'A' objects>, '__slots__': ['x']})
      >>> A.__dict__['x']
      <member 'x' of 'A' objects>
      >>> a = A()
      >>> A.__dict__['x'].__get__(a, A)
      Traceback (most recent call last):
        File "<stdin>", line 1, in <module>
      AttributeError: x
      >>> A.__dict__['x'].__set__(a, 'foobar')
      >>> A.__dict__['x'].__get__(a, A)
      'foobar'
      >>> a.x
      'foobar'
      

      您不能自己创建这些额外的描述符。即使可以,您也不能为此类生成的实例上的额外插槽引用分配更多内存空间,因为这是存储在该类的 C 结构中的信息,而不是以 Python 代码可访问的方式。

      这都是因为__slots__ 只是将构成 Python 实例的元素的低级处理扩展到 Python 代码; regular Python 实例上的__dict____weakref__ 属性始终作为槽实现:

      >>> class Regular: pass
      ... 
      >>> Regular.__dict__['__dict__']
      <attribute '__dict__' of 'Regular' objects>
      >>> Regular.__dict__['__weakref__']
      <attribute '__weakref__' of 'Regular' objects>
      >>> r = Regular()
      >>> Regular.__dict__['__dict__'].__get__(r, Regular) is r.__dict__
      True
      

      所有 Python 开发人员在这里所做的只是扩展系统以使用任意名称添加更多此类插槽,这些名称取自正在创建的类的 __slots__ 属性,以便您可以节省内存;字典比对插槽中的值的简单引用占用更多的内存。通过指定__slots__,您可以禁用__dict____weakref__ 插槽,除非您在__slots__ 序列中明确包含这些插槽。

      那么扩展槽的唯一方法就是子类化;您可以使用type() 函数或使用工厂函数动态创建子类:

      def extra_slots_subclass(base, *slots):
          class ExtraSlots(base):
              __slots__ = slots
          ExtraSlots.__name__ = base.__name__
          return ExtraSlots
      

      【讨论】:

      • 当然,只是来这里玩弄我的答案;-)。不过,对于带有描述符的有趣演示 +1。
      • @mgilson: :-P 你的在这张照片中同样有效,所以我已经投票给你了。
      • 感谢您的支持 :-)。我开始怀疑我是否陷入困境并应该删除我的答案......无论如何,最近我一直在寻找任何机会去挖掘 C 代码,因为,为什么不呢?
      【解决方案3】:

      创建类后不能修改__slots__ 属性。这是因为它会导致奇怪的行为。

      想象一下。

      class A:
          __slots__ = ["x"]
      
      a = A()
      A.__slots__.append("y")
      a.y = None 
      

      在这种情况下应该发生什么?最初没有为第二个槽分配空间,但根据槽属性,a 应该能够为y 分配空间。

      __slots__ 并不是要保护可以访问和不能访问的名称。而__slots__ 是关于减少对象的内存占用。通过尝试修改 __slots__,您将破坏 __slots__ 旨在实现的优化。

      __slots__ 如何减少内存占用

      通常,对象的属性存储在dict 中,这本身需要相当多的内存。如果您要创建数百万个对象,那么这些 dicts 所需的空间就会变得令人望而却步。 __slots__ 通知生成类对象的 python 机制,此类实例引用的属性只有这么多,以及属性的名称是什么。因此,该类可以通过将属性直接存储在实例上而不是dict 中来进行优化。它将(指向)属性的内存直接放在对象上,而不是为对象创建一个新的dict

      【讨论】:

      • 在玩了几年描述符之后,我开始将描述符视为它自己的字典,例如:def __get__(dsc, obj, cls=None): return dsc if cls is None else dsc.Map[obj] 其中Map 将是描述符实例上的字典,包含与 obj 实例关联的值......这让我质疑描述符是否真的能节省内存。
      • @Tcll 您混淆了插槽和描述符。插槽是描述符的一种特殊形式。带有槽的对象有点像一个可变的命名元组,其中属性名称只是在给定索引处获取元素的便捷方法。插槽描述符在 C 级别实现,本质上只是 *(self + attr_index)
      • 诚然,我不完全确定 member_descriptor 类(插槽属性)是如何工作的......所以你说它基本上是一个指向使用提供的实例进行属性索引的值的指针??
      【解决方案4】:

      把这个问题的答案和related question 放在一起,我想强调一下这个问题的解决方案:

      您可以某种修改__slots__,方法是创建一个具有相同名称的子类,然后用其子类替换父类。请注意,您可以为在任何模块中声明和使用的类执行此操作,而不仅仅是您的!


      考虑以下声明了一些类的模块:

      module.py:
      class A(object):
          # some class a user should import
          __slots__ = ('x', 'b')
      
          def __init__(self):
              self.b = B()
      
      class B(object):
          # let's suppose we can't use it directly,
          # it's returned as a part of another class
          __slots__ = ('z',)
      

      以下是向这些类添加属性的方法:

      >>> import module
      >>> from module import A
      >>>
      >>> # for classes imported into your module:
      >>> A = type('A', (A,), {'__slots__': ('foo',)})
      >>> # for classes which will be instantiated by the `module` itself:
      >>> module.B = type('B', (module.B,), {'__slots__': ('bar',)})
      >>>
      >>> a = A()
      >>> a.x = 1
      >>> a.foo = 2
      >>>
      >>> b = a.b
      >>> b.z = 3
      >>> b.bar = 4
      >>>
      

      但是,如果您使用 module 从某个第三方模块接收类实例怎么办?

      module_3rd_party.py:
      from module import A
      
      def get_instance():
          return A()
      

      没问题,它也可以!唯一的区别是您可能需要在导入第三方模块之前对它们进行修补(以防它从module 导入类):

      >>> import module
      >>>
      >>> module.A = type('A', (module.A,), {'__slots__': ('foo',)})
      >>> module.B = type('B', (module.B,), {'__slots__': ('bar',)})
      >>>
      >>> # note that we import `module_3rd_party` AFTER we patch the `module`
      >>> from module_3rd_party import get_instance
      >>>
      >>> a = get_instance()
      >>> a.x = 1
      >>> a.foo = 2
      >>>
      >>> b = a.b
      >>> b.z = 3
      >>> b.bar = 4
      >>>
      

      之所以有效,是因为 Python 只导入一次模块,然后在所有其他模块之间共享它们,因此您对模块所做的更改会影响沿您的模块运行的所有代码。

      【讨论】:

        猜你喜欢
        • 2023-03-09
        • 2015-07-16
        • 2019-08-11
        • 2011-05-03
        • 2021-01-27
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多