我正在设计一个装饰器来为不可变类实现一些行为。我喜欢从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存储了什么?
答案 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
和指定除type
或DefineMeta
以外的元类的类型继承,则可能会导致错误。由于我们只需要将元类挂钩到类创建中,但以后没有使用它,因此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__
要做的事情。