在Python 3中动态更改__slots__

时间:2015-01-12 17:18:40

标签: python python-3.x slots

假设我有一个__slots__

的班级
class A:
    __slots__ = ['x']

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

现在我要更改__slots__的{​​{1}}。

A
A.__slots__.append('y') print(A.__slots__) # ['x', 'y'] b = A() b.x = 1 # OK b.y = 1 # AttributeError (why?) b更改后创建了

__slots__,因此Python原则上可以为A分配内存。为什么没有?

如何正确修改类的b.y,以便新实例具有修改后的属性?

4 个答案:

答案 0 :(得分:11)

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

  通过为每个变量名创建描述符(实现描述符),在类级别实现

__slots__。因此,类属性不能用于为__slots__定义的实例变量设置默认值;否则,class属性将覆盖描述符赋值。

您可以在班级__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代码; 常规 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 :(得分:3)

在我看来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 :(得分:2)

创建类后,您无法修改__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

答案 3 :(得分:0)

将这个问题和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仅导入模块一次,然后在所有其他模块之间共享它们,因此您对模块所做的更改会影响沿您的模块运行的所有代码。