Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Behavior of __new__ in a metaclass (also in context of inheritance)

Ok, obviously __new__ in a metaclass runs when an instance of the metaclass i.e. a class object is instantiated, so __new__ in a metaclass provides a hook to intercept events (/code that runs) at class definition time (e.g. validating/enforcing rules for class attributes such as methods etc.).

Many online examples of __new__ in a metaclass return an instance of the type constructor from __new__, which seems a bit problematic since this blocks __init__ (docs: "If __new__() does not return an instance of cls, then the new instance’s __init__() method will not be invoked").

While tinkering with return values of __new__ in a metaclass I came across some somewhat strange cases which I do not fully understand, e.g.:

class Meta(type):
    
    def __new__(self, name, bases, attrs):
        print("Meta __new__ running!")
        # return type(name, bases, attrs)                     # 1. 
        # return super().__new__(self, name, bases, attrs)    # 2.
        # return super().__new__(name, bases, attrs)          # 3.  
        # return super().__new__(type, name, bases, attrs)    # 4. 
        # return self(name, bases, attrs)                     # 5.

    def __init__(self, *args, **kwargs):
        print("Meta __init__ running!")
        return super().__init__(*args, **kwargs)
    
class Cls(metaclass=Meta):
    pass
  1. This is often seen in examples and generally works, but blocks __init__
  2. This works and __init__ also fires; but why pass self to a super() call? Shouldn't self/cls get passed automatically with super()?
  3. This throws a somewhat strange error I can't really make sense of: TypeError: type.__new__(X): X is not a type object (str); what is X? shouldn't self be auto-passed?
  4. The error of 3. inspired me to play with the first arg of the super() call, so I tried to pass type directly; this also blocks __init__. What is happening here?
  5. tried just for fun; this leads to a RecursionError

Also especially cases 1. and 2. appear to have quite profound implications for inheriting from classes bound to metaclasses:

class LowerCaseEnforcer(type):
    """ Allows only lower case names as class attributes! """

    def __new__(self, name, bases, attrs): 
        for name in attrs:
            if name.lower() != name:
                raise TypeError(f"Inappropriate method name: {name}")
            
        # return type(name, bases, attrs)                    # 1.
        # return super().__new__(self, name, bases, attrs)   # 2.

    class Super(metaclass=LowerCaseEnforcer):
        pass
    
    class Sub(Super):
        
        def some_method(self):
            pass
    
        ## this will error in case 2 but not case 1
        def Another_method(self):
            pass
  1. expected behavior: metaclass is bound to superclass, but not to subclass
  2. binds the superclass /and/ subclasses to the metaclass; ?

I would much appreciate if someone could slowly and kindly explain what exactly is going on in the above examples! :D

like image 302
upgrd Avatar asked Apr 15 '21 09:04

upgrd


People also ask

What is __ metaclass __ in Python?

A metaclass in Python is a class of a class that defines how a class behaves. A class is itself an instance of a metaclass. A class in Python defines how the instance of the class will behave. In order to understand metaclasses well, one needs to have prior experience working with Python classes.

What does the __ New__ method do?

Summary. The __new__() is a static method of the object class. When you create a new object by calling the class, Python calls the __new__() method to create the object first and then calls the __init__() method to initialize the object's attributes.

What is a metaclass in programming?

In object-oriented programming, a metaclass is a class whose instances are classes. Just as an ordinary class defines the behavior of certain objects, a metaclass defines the behavior of certain classes and their instances. Not all object-oriented programming languages support metaclasses.

Which method sets the metaclass of class C to M in Python?

In order to set metaclass of a class, we use the __metaclass__ attribute.

Why can’t I inherit from two metaclasses in Python?

Here you will get the below error message while trying to inherit from two different metaclasses. This is because Python can only have one metaclass for a class. Here, class C can’t inherit from two metaclasses, which results in ambiguity. In most cases, we don’t need to go for a metaclass, normal code will fit with the class and object.

What does __new__ do in metaclass?

Metaclass' __new__ The method __new__ is what is called to create a new instance. Thus its first argument is not an instance, since none has been created yet, but rather the class itself. In the case of a metaclass, __new__ is expected to return an instance of your metaclass, that is a class.

What is the primary metaclass of a class?

Summary. Normal classes that are designed using class keyword have type as their metaclasses, and type is the primary metaclass. Metaclasses are a powerful tool in Python that can overcome many limitations. But most of the developers have a misconception that metaclasses are difficult to grasp.

Can a class have no inheritance from any parent class?

No inheritance from any parent class is specified, and nothing is initially placed in the namespace dictionary. This is the simplest class definition possible: Here, <bases> is a tuple with a single element Foo, specifying the parent class that Bar inherits from.


1 Answers

It is simpler than what you got too.

As you have noted, the correct thing to do is your 2 above:

return super().__new__(self, name, bases, attrs)    # 2.

Here it goes: __new__ is a special method - although in certain documentations, even part of the official documentation, it is described as being a classmethod, it is not quite so: it, as an object, behaves more like a static method - in a sense that Python does not automatically fill the first parameter when one calls MyClass.__new__() - i.e., you'd have to call MyClass.__new__(MyClass) for it to work. (I am a step back here - this info applies to all classes: metaclasses and ordinary classes).

When you call MyClass() to create a new instance, then Python will call MyClass.__new__ and insert the cls parameter as first parameter.

With metaclasses, the call to create a new instance of the metaclass is triggered by the execution of the class statement and its class body. Likewise, Python fills in the first parameter to Metaclass.__new__, passing the metaclass itself.

When you call super().__new__ from within your metaclass' __new__ you are in the same case of one calling __new__ manually: the parameter specifying which class' that __new__ should apply have to be explicitly filled.

Now, what is confusing you is that you are writting the first parameter to __new__ as self - which would be correct if it were an instance of the metaclass (i.e. an ordinary class). As it is, that parameter is a reference to the metaclass itself.

The docs does not inform an official, or recomended name for the first parameter of a metaclass __new__, but usually it is something along mcls, mcs, metaclass, metacls - to make it different from cls which is the usuall name for the first parameter of a non-metaclass __new__ method. In a metaclass, the "class" - cls is what is created by the ultimate call to type.__new__ (either hardcoded, or using super()) the return of it is the new-born class (it can be further modified in the __new__ method after the call to the superclass) - and when returned, the call to __init__ takes place normally.

So, I will just comment further the use of trying to call type(name, bases, namespace) instead of type.__new__(mcls, name, bases, namespace): the first form will just create a plain class, as if the metaclass had not been used at all - (lines in the metaclass __new__ that modify the namespace or bases, of course, have their effect. But the resulting class will have type as its metaclass, and subclasses of it won't call the metaclass at all. (For the record, it works as a "pre-class decorator" - which can act on class parameters before it is created, and it could even be an ordinary function, instead of a class with a __new__ method - the call to type is what will create the new class after all)

A simple way to check if the metaclass is "bound" to your class is to check its type with type(MyClass) or MyClass.__class__ .

like image 117
jsbueno Avatar answered Nov 09 '22 05:11

jsbueno