为什么通过装饰器和 class 主体进行的 class.__slots__ 赋值存在差异?

Why differences in class.__slots__ assignment via decorator vs. class body?

我正在开发一个装饰器来为不可变 class 实现一些行为。我想要一个 class 从 namedtuple 继承(具有属性不变性)并且还想添加一些新方法。 Like this ...但正确地阻止了将新属性分配给新的 class.

继承namedtuple时,需要定义__new__并设置__slots__为空元组(保持不变性):

def define_new(clz):
    def __new(cls, *args, **kwargs):
        return super(clz, cls).__new__(cls, *args, **kwargs)

    clz.__new__ = staticmethod(__new) # delegate namedtuple.__new__ to namedtuple
    return clz

@define_new
class C(namedtuple('Foo', "a b c")):
    __slots__ = () # Prevent assignment of new vars
    def foo(self): return "foo"

C(1,2,3).x = 123 # Fails, correctly

...太棒了。但现在我想将 __slots__ 赋值移动到装饰器中:

def define_new(clz):
    def __new(cls, *args, **kwargs):
        return super(clz, cls).__new__(cls, *args, **kwargs)

    #clz.__slots__ = ()
    clz.__slots__ = (123) # just for testing

    clz.__new__ = staticmethod(__new)
    return clz

@define_new
class C(namedtuple('Foo', "a b c")):
    def foo(self): return "foo"

c = C(1,2,3)
print c.__slots__ # Is the (123) I assigned!
c.x = 456         # Assignment succeeds! Not immutable.
print c.__slots__ # Is still (123)

这有点令人惊讶。

为什么将 __slots__ 的赋值移动到装饰器中会导致行为发生变化?

如果我打印 C.__slots__,我得到我分配的对象。 x 存储了什么?

代码无效,因为 __slots__ 不是在 运行 时咨询的正常 class 属性。它是 class 的基础 属性,影响其每个实例的内存布局,因此必须在创建 class 时知道并在其整个生命周期中保持静态。虽然 Python(可能是为了向后兼容)允许稍后分配给 __slots__,但分配对现有或未来实例的行为没有影响。

如何设置 __slots__

class作者确定的__slots__值在创建class对象时传递给class构造函数。这是在执行 class 语句时完成的;例如:

class X:
    __slots__ = ()

上面的语句等同于1创建一个class对象并将其赋值给X:

X = type('X', (), {'__slots__': ()})

type 对象是 metaclass,在调用时创建和 returns 一个 class 的工厂。 metaclass 调用接受类型的名称、它的超classes 和它的定义字典。定义字典的大部分内容也可以稍后赋值定义字典包含影响class实例低级布局的指令。正如您所发现的,稍后分配给 __slots__ 根本没有效果。

从外部设置__slots__

要修改 __slots__ 以便它被 Python 拾取,必须在创建 class 时指定它。这可以通过 metaclass 来完成,该类型负责实例化类型。 metaclass 驱动 class 对象的创建,它可以确保 __slots__ 在调用构造函数之前进入 class 定义字典:

class DefineNew(type):
    def __new__(metacls, name, bases, dct):

        def __new__(cls, *new_args, **new_kwargs):
            return super(defcls, cls).__new__(cls, *new_args, **new_kwargs)

        dct['__slots__'] = ()
        dct['__new__'] = __new__

        defcls = super().__new__(metacls, name, bases, dct)
        return defcls

class C(namedtuple('Foo', "a b c"), metaclass=DefineNew):
    def foo(self):
        return "foo"

测试结果符合预期:

>>> c = C(1, 2, 3)
>>> c.foo()
'foo'
>>> c.bar = 1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'C' object has no attribute 'bar'

Metaclass 混合陷阱

请注意,C 类型对象本身将是 DefineMeta 实例 - 这并不奇怪,因为这是从 a 的定义得出的元class。但是,如果您从 C 和指定元 class 而不是 typeDefineMeta 的类型继承,这可能会导致错误。由于我们只需要 metaclass 挂钩到 class 的创建中,但以后不会使用它,因此 C 并不是必须作为 [=28= 的实例创建的] - 我们可以改为使它成为 type 的实例,就像任何其他 class 一样。这是通过更改行来实现的:

        defcls = super().__new__(metacls, name, bases, dct)

至:

        defcls = type.__new__(type, name, bases, dct)

__new____slots__的注入将保留,但C将是最普通的类型,默认metaclass。

总之...

定义一个简单地调用 superclass __new____new__ 总是多余的——大概真正的代码也会在注入的 __new__ 中做一些不同的事情,例如为命名元组提供默认值。


1 在实际的 class 定义中,编译器向 class 字典添加了一些额外的项目,例如模块名称。这些很有用,但它们不会像 __slots__ 那样从根本上影响 class 定义。如果 X 有方法,它们的函数对象也将包含在由函数名称键入的字典中——作为在 class 定义命名空间字典中执行 def 语句的副作用自动插入。

__slots__ 必须在 class 创建期间存在。它会影响 class 实例的内存布局,这不是您可以随意更改的内容。 (想象一下,如果您已经拥有 class 的实例,并且您试图在此时重新分配 class 的 __slots__;实例将全部中断。)基于内存布局的处理__slots__ 仅在 class 创建期间发生。

在装饰器中分配 __slots__ 已经来不及做任何事情了。它必须在 class 创建之前发生,在 class 正文或元 class __new__.

此外,您的define_new毫无意义; namedtuple.__new__ 已经做了您需要 __new__ 做的事情。