为什么通过装饰器与类主体在class .__ slots__分配中存在差异?

use*_*956 0 python decorator class-method namedtuple

我正在一个装饰器上实现不可变类的某些行为。我想要一个从namedtuple继承的类(具有属性不变性),并且还想添加一些新方法。像这样 ...但是正确防止将新属性分配给新类。

从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
Run Code Online (Sandbox Code Playgroud)

太好了 但是现在我想将__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)
Run Code Online (Sandbox Code Playgroud)

这有点令人惊讶。

为什么将的分配__slots__移入装饰器会导致行为发生变化?

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

use*_*342 5

该代码不起作用,因为__slots__在运行时未咨询普通的类属性。它是类的基本属性,会影响其每个实例的内存布局,因此在创建类时必须知道该类,并且在其整个生命周期内都保持静态。尽管Python(大概是为了向后兼容)允许__slots__以后分配,但分配对现有实例或将来实例的行为没有影响。

如何__slots__设置

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

class X:
    __slots__ = ()
Run Code Online (Sandbox Code Playgroud)

上面的语句等效于1,它创建一个类对象并将其分配给X

X = type('X', (), {'__slots__': ()})
Run Code Online (Sandbox Code Playgroud)

type对象是元类,创建并调用时返回类工厂。元类调用接受类型的名称,其超类及其定义dict。定义dict的大多数内容也可以在以后分配。定义dict包含影响类实例的底层布局的指令。如您所知,以后分配给__slots__根本没有效果。

__slots__从外面设置

要进行修改__slots__以便被Python拾取,必须在创建类时指定它。这可以通过metaclass(负责实例化类型的类型)来完成。元类驱动类对象的创建,并且可以确保__slots__在调用构造函数之前将其放入类定义dict中:

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"
Run Code Online (Sandbox Code Playgroud)

测试结果符合预期:

>>> 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'
Run Code Online (Sandbox Code Playgroud)

元类混合陷阱

请注意,C类型对象本身将是- 的实例DefineMeta这并不奇怪,因为它来自元类的定义。但这可能会导致错误,如果您同时继承两者C和指定了除typeor 以外的元类的类型DefineMeta。由于我们只需要将元类挂接到类的创建上,而以后不再使用它,则不必严格C将其创建为的实例DefineMeta-我们可以type像其他任何类一样将其创建为的实例。这是通过更改以下行来实现的:

        defcls = super().__new__(metacls, name, bases, dct)
Run Code Online (Sandbox Code Playgroud)

至:

        defcls = type.__new__(type, name, bases, dct)
Run Code Online (Sandbox Code Playgroud)

的注入__new__,并__slots__会保留,但C将是一个最普通的类型使用默认元类。

结论...

定义一个__new__简单地调用超类__new__的a总是多余的-假定实际代码还会在注入中做一些不同的事情__new__,例如,为namedtuple提供默认值。


1 在实际的类定义中,编译器在类字典中添加了几个附加项,例如模块名称。这些是有用的,但是它们不会以根本的方式影响类定义__slots__。如果X有方法,则它们的函数对象也将包含在以函数名作为键的dict中-作为def在类定义名称空间dict 中执行该语句的副作用而自动插入。