I am a bit surprised by the method call order and the different arguments when overriding new
and init
in a metaclass. Consider the following:
class AT(type):
def __new__(mcs, name, bases, dct):
print(f"Name as received in new: {name}")
return super().__new__(mcs, name + 'HELLO', bases + (list,), dct)
def __init__(cls, name, bases, dct):
print(f"Name as received in init: {name}")
pass
class A(metaclass=AT):
pass
A.__name__
The output is:
Name as received in new: A
Name as received in init: A
'AHELLO'
In short I would have expected init
to receive AHELLO
with the argument name
.
I imagined that __init__
was called by super().__new__
: if the call is not done in the overridden __new__
then my __init__
is not called.
Could someone clarify how __init__
is called in this case?
For information my use case for this is that I wanted to make creation of classes, in a special case, easier at runtime by providing only a single "base" class (and not a tuple), I then added this code in __new__
:
if not isinstance(bases, tuple):
bases = (bases, )
however, I found out that I also need to add it in __init__
.
Your
__init__
method is obviously called and the reason for that is because your__new__
method is returning an instance of your class.From https://docs.python.org/3/reference/datamodel.html#object.new:
As you can see the arguments passed to
__init__
are those passed to__new__
method's caller not when you call it usingsuper
. It's a little bit vague but that's what it means if you read it closely.And regarding the rest it works just as expected:
The fact is that what orchestrates the call of
__new__
and__init__
of an ordinary class is the__call__
method on its metaclass. The code in the__call__
method oftype
, the default metatype, is in C, but the equivalent of it in Python would be:That takes place for most object instantiation in Python, including when instantiating classes themselves - the metaclass is implicitly called as part of a class statement. In this case, the
__new__
and__init__
called fromtype.__call__
are the methods on the metaclass itself. And in this case,type
is acting as the "metametaclass" - a concept seldom needed, but it is what creates the behavior you are exploring.When creating classes,
type.__new__
will be responsible for calling the class (not the metaclass)__init_subclass__
, and its descriptors'__set_name__
methods - so, the "metametaclass"__call__
method can't control that.So, if you want the args passed to the metaclass
__init__
to be programmatically modified, the "normal" way will be to have a "metametaclass", inheriting fromtype
and distinct from your metaclass itself, and override its__call__
method:Of course that is a way to get to go closer to "turtles all way to the bottom" than anyone would ever like in production code.
An alternative is to keep the modified name as an attribute on the metaclass, so that its
__init__
method can take the needed information from there, and ignore the name passed in from its own metaclass'__call__
invocation. The information channel can be an ordinary attribute on the metaclass instance. Well - it happens that the "metaclass instance" is the class being created itself - and oh, see - that the name passed totype.__new__
already gets recorded in it - on the__name__
atribute.In other words, all you have to do to use a class name modified in a metaclass
__new__
method in its own__init__
method, is to ignore the passed inname
argument, and usecls.__name__
instead: