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

时间:2018-02-01 17:13:41

标签: python decorator class-method namedtuple

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

从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存储了什么?

2 个答案:

答案 0 :(得分:5)

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

如何设置__slots__

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

class X:
    __slots__ = ()

以上语句与 1 等效,用于创建类对象并将其分配给X

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

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

从外部设置__slots__

要修改__slots__以便Python选择它,必须在创建类时指定它。这可以使用元类来完成,元类是负责实例化类型的类型。元类驱动类对象的创建,它可以确保__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"

预期测试结果:

>>> 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'

元素混合陷阱

请注意,C类型对象本身就是DefineMeta实例 - 这并不奇怪,因为它来自元类的定义。但是,如果您从C和指定除typeDefineMeta以外的元类的类型继承,则可能会导致错误。由于我们只需要将元类挂钩到类创建中,但以后没有使用它,因此C并不一定要将DefineMeta创建为type的实例 - 我们可以将其作为实例 defcls = super().__new__(metacls, name, bases, dct) 的任何一个,就像任何其他类一样。这可以通过更改行来实现:

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

为:

__new__

__slots__C的注入将保留,但__new__将是具有默认元类的最普通类型。

总之......

定义一个只调用超类__new__的{​​{1}}总是多余的 - 可能真正的代码也会在注入的__new__中执行不同的操作,例如提供namedtuple的默认值。

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

< / p>

答案 1 :(得分:2)

在课堂创作过程中必须出现

__slots__。它会影响一个类实例的内存布局,这不是你可以随意改变的东西。 (想象一下,如果您已经拥有该类的实例,并且您尝试在该点重新分配该类__slots__;实例将全部中断。)仅基于__slots__的内存布局的处理在课堂创作过程中发生。

在装饰器中分配__slots__为时已晚,无法做任何事情。它必须在类创建之前,在类体或元类__new__中发生。

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