9

假设我有一堂课__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__ofA改变之后创建的,因此 Python 原则上可以为b.y. 为什么没有?

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

4

4 回答 4

19

创建类后,您不能动态更改__slots__属性,不。这是因为该值用于为每个插槽创建特殊描述符。__slots__文档中:

__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 代码;常规Python 实例上的__dict__and__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__and__weakref__插槽,除非您在__slots__序列中明确包含这些插槽。

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

def extra_slots_subclass(base, *slots):
    class ExtraSlots(base):
        __slots__ = slots
    ExtraSlots.__name__ = base.__name__
    return ExtraSlots
于 2015-01-12T17:52:22.117 回答
3

在我看来,一个类型变成__slots__了一个元组,这是它的第一个动作顺序之一。然后它将元组存储在扩展类型 object 上。由于在这一切之下,python 正在查看 a tuple,因此无法对其进行变异。事实上,我什至不确定你是否可以访问它,除非你首先将一个元组传递给实例。

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

不能修改__slots__并期望将其显示在某个地方(实际上 - 从可读性的角度来看,您可能并不这样做,对吧?)...

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

>>> class C(A):
...   __slots__ = ['z']
... 
>>> c = C()
>>> c.x = 1
>>> c.z = 1
于 2015-01-12T17:51:42.790 回答
2

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

想象以下。

class A:
    __slots__ = ["x"]

a = A()
A.__slots__.append("y")
a.y = None 

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

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

__slots__ 如何减少内存占用

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

于 2015-01-12T18:02:51.240 回答
1

将这个问题和相关问题的答案放在一起,我想强调一下这个问题的解决方案:

您可以通过创建具有相同名称的子类然后用其子类替换父类来进行修改。__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 只导入一次模块,然后在所有其他模块之间共享它们,因此您对模块所做的更改会影响您运行的所有代码。

于 2019-07-25T19:24:02.357 回答