Temperosa
Temperosa

Reputation: 168

Unexpected behavior of setattr() inside Metaclass __init__

Here is a basic Metaclass intending to provide a custom __init__:

class D(type):
    def __init__(cls, name, bases, attributes):
        super(D, cls).__init__(name, bases, attributes)
        for hook in R.registered_hooks:
            setattr(cls, hook.__qualname__, hook)

The for loop iterates over a list of functions, calling setattr(cls, hook.__qualname__, hook) for each of it.

Here is a Class that use the above Metaclass:

class E(metaclass=D):
    def __init__(self):
        pass

The weird thing:

E().__dict__   prints {}

But when I call

`E.__dict__   prints {'f1': <function f1 at 0x001>, 'f2': <function f2 at 0x002>}`

I was expecting to add those function as attributes to the instance attributes, since the __init__ provides a custom initialization for a Class but it seems like the attributes were added to Class attributes.

What is the cause of this and how can I add attributes to the instance, in this scenario? Thanks!

Upvotes: 0

Views: 173

Answers (3)

Grant Palmer
Grant Palmer

Reputation: 208

If you're using a metaclass, that means you're changing the definition of a class, which indirectly affects the attributes of instances of that class. Assuming you actually want to do this:

You're creating a custom __init__ method. As with normal instantiation, __init__ is called on an object that has been already created. Generally when you're doing something meaningful with metaclasses, you'll want to add to the class's dict before instantiation, which is done in the __new__ method to avoid issues that may come up in subclassing. It should look something like this:

class D(type):

    def __new__(cls, name, bases, dct):
        for hook in R.registered_hooks:
            dct[hook.__qualname__] = hook
        return super(D, cls).__new__(cls, name, bases, dct)

This will not modify the __dict__ of instances of that class, but attribute resolution for instances also looks for class attributes. So if you add foo as an attribute for E in the metaclass, E().foo will return the expected value even though it's not visible in E().__dict__.

Upvotes: 2

Olivier Melan&#231;on
Olivier Melan&#231;on

Reputation: 22304

For __init__ to be called on the instantiation of an object obj, you need to have defined type(obj).__init__. By using a metaclass, you defined type(type(obj)).__init__. You are working at the wrong level.

In this case, you only need inheritance, not a metaclass.

class D():
    def __init__(self, *args):
        print('D.__init__ was called')

class E(D):
    pass

e = E() # prints: D.__init__ was called

Note that you will still find that e.__dict__ is empty.

print(e.__dict__) # {}

This is because you instances have no attributes, methods are stored and looked up in the class.

Upvotes: 2

chepner
chepner

Reputation: 531075

They are added as instance attributes. The instance, though, is the class, as an instance of the metaclass. You aren't defining a __init__ method for E; you are defining the method that type uses to initialize E itself.

If you just want attributes added to the instance of E, you're working at the wrong level; you should define a mixin and use multiple inheritance for that.

Upvotes: 3

Related Questions