I'm working on a decorator to implement some behaviors for an immutable class. I'd like a class to inherit from namedtuple (to have attribute immutability) and also want to add some new methods. Like this ... but correctly preventing new attributes being assigned to the new class.
When inheriting from namedtuple, you should define __new__
and set __slots__
to be an empty tuple (to maintain immutability):
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
... great. But now I'd like to move the __slots__
assignment into the decorator:
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)
Which is a little surprising.
Why has moving the assignment of __slots__
into the decorator caused a change in behavior?
If I print C.__slots__
, I get the object I assigned. What do the x get stored?
The code doesn't work because __slots__
is not a normal class property consulted at run-time. It is a fundamental property of the class that affects the memory layout of each of its instances, and as such must be known when the class is created and remain static throughout the its lifetime. While Python (presumably for backward compatibility) allows assigning to __slots__
later, the assignment has no effect on the behavior of existing or future instances.
__slots__
is setThe value of __slots__
determined by the class author is passed to the class constructor when the class object is being created. This is done when the class
statement is executed; for example:
class X:
__slots__ = ()
The above statement is equivalent1 to creating a class object and assigning it to X
:
X = type('X', (), {'__slots__': ()})
The type
object is the metaclass, the factory that creates and returns a class when called. The metaclass invocation accepts the name of the type, its superclasses, and its definition dict. Most of the contents of the definition dict can also be assigned later The definition dict contains directives that affect low-level layour of the class instances. As you discovered, later assignment to __slots__
simply has no effect.
__slots__
from the outsideTo modify __slots__
so that it is picked up by Python, one must specify it when the class is being created. This can be accomplished with a metaclass, the type responsible for instantiating types. The metaclass drives the creation of the class object and it can make sure __slots__
makes its way into the class definition dict before the constructor is invoked:
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"
Testing results in the expected:
>>> 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'
Note that the C
type object will itself be an instance of DefineMeta
- which is not surprising, since that follows from the definition of a metaclass. But this might cause an error if you ever inherit from both C
and a type that specifies a metaclass other than type
or DefineMeta
. Since we only need the metaclass to hook into class creation, but are not using it later, it is not strictly necessary for C
to be created as an instance of DefineMeta
- we can instead make it an instance of type
, just like any other class. This is achieved by changing the line:
defcls = super().__new__(metacls, name, bases, dct)
to:
defcls = type.__new__(type, name, bases, dct)
The injection of __new__
and __slots__
will remain, but C
will be a most ordinary type with the default metaclass.
Defining a __new__
which simply calls the superclass __new__
is always superfluous - presumably the real code will also do something different in the injected __new__
, e.g. provide the default values for the namedtuple.
__slots__
does. If X
had methods, their function objects would also be included in the dict keyed by function name - automatically inserted as a side effect of executing the def
statement in the class definition namespace dict.