Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using the __call__ method of a metaclass instead of __new__?

When discussing metaclasses, the docs state:

You can of course also override other class methods (or add new methods); for example defining a custom __call__() method in the metaclass allows custom behavior when the class is called, e.g. not always creating a new instance.

[Editor's note: This was removed from the docs in 3.3. It's here in 3.2: Customizing class creation]

My questions is: suppose I want to have custom behavior when the class is called, for example caching instead of creating fresh objects. I can do this by overriding the __new__ method of the class. When would I want to define a metaclass with __call__ instead? What does this approach give that isn't achievable with __new__?

like image 359
Eli Bendersky Avatar asked Aug 06 '11 12:08

Eli Bendersky


People also ask

What does __ call __ do in Python?

The __call__ method enables Python programmers to write classes where the instances behave like functions and can be called like a function. When the instance is called as a function; if this method is defined, x(arg1, arg2, ...) is a shorthand for x.

What is the use of 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 is __ new __ in Python?

The __new__() is a static method of the object class. It has the following signature: object.__new__(class, *args, **kwargs) Code language: Python (python) The first argument of the __new__ method is the class of the new object that you want to create.

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.


2 Answers

The direct answer to your question is: when you want to do more than just customize instance creation, or when you want to separate what the class does from how it's created.

See my answer to Creating a singleton in Python and the associated discussion.

There are several advantages.

  1. It allows you to separate what the class does from the details of how it's created. The metaclass and class are each responsible for one thing.

  2. You can write the code once in a metaclass, and use it for customizing several classes' call behavior without worrying about multiple inheritance.

  3. Subclasses can override behavior in their __new__ method, but __call__ on a metaclass doesn't have to even call __new__ at all.

  4. If there is setup work, you can do it in the __new__ method of the metaclass, and it only happens once, instead of every time the class is called.

There are certainly lots of cases where customizing __new__ works just as well if you're not worried about the single responsibility principle.

But there are other use cases that have to happen earlier, when the class is created, rather than when the instance is created. It's when these come in to play that a metaclass is necessary. See What are your (concrete) use-cases for metaclasses in Python? for lots of great examples.

like image 113
agf Avatar answered Sep 21 '22 22:09

agf


The subtle differences become a bit more visible when you carefully observe the execution order of these methods.

class Meta_1(type):     def __call__(cls, *a, **kw):         print "entering Meta_1.__call__()"         rv = super(Meta_1, cls).__call__(*a, **kw)         print "exiting Meta_1.__call__()"         return rv  class Class_1(object):     __metaclass__ = Meta_1     def __new__(cls, *a, **kw):         print "entering Class_1.__new__()"         rv = super(Class_1, cls).__new__(cls, *a, **kw)         print "exiting Class_1.__new__()"         return rv      def __init__(self, *a, **kw):         print "executing Class_1.__init__()"         super(Class_1,self).__init__(*a, **kw) 

Note that the code above doesn't actually do anything other than log what we're doing. Each method defers to its parent implementation i.e. its default. So beside logging it's effectively as if you had simply declared things as follows:

class Meta_1(type): pass class Class_1(object):     __metaclass__ = Meta_1 

And now let's create an instance of Class_1

c = Class_1() # entering Meta_1.__call__() # entering Class_1.__new__() # exiting Class_1.__new__() # executing Class_1.__init__() # exiting Meta_1.__call__() 

Therefore if type is the parent of Meta_1 we can imagine a pseudo implementation of type.__call__() as such:

class type:     def __call__(cls, *args, **kwarg):          # ... a few things could possibly be done to cls here... maybe... or maybe not...          # then we call cls.__new__() to get a new object         obj = cls.__new__(cls, *args, **kwargs)          # ... a few things done to obj here... maybe... or not...          # then we call obj.__init__()         obj.__init__(*args, **kwargs)          # ... maybe a few more things done to obj here          # then we return obj         return obj 

Notice from the call order above that Meta_1.__call__() (or in this case type.__call__()) is given the opportunity to influence whether or not calls to Class_1.__new__() and Class_1.__init__() are eventually made. Over the course of its execution Meta_1.__call__() could return an object that hasn't even been touched by either. Take for example this approach to the singleton pattern:

class Meta_2(type):     __Class_2_singleton__ = None     def __call__(cls, *a, **kw):         # if the singleton isn't present, create and register it         if not Meta_2.__Class_2_singleton__:             print "entering Meta_2.__call__()"             Meta_2.__Class_2_singleton__ = super(Meta_2, cls).__call__(*a, **kw)             print "exiting Meta_2.__call__()"         else:             print ("Class_2 singleton returning from Meta_2.__call__(), "                     "super(Meta_2, cls).__call__() skipped")         # return singleton instance         return Meta_2.__Class_2_singleton__  class Class_2(object):     __metaclass__ = Meta_2     def __new__(cls, *a, **kw):         print "entering Class_2.__new__()"         rv = super(Class_2, cls).__new__(cls, *a, **kw)         print "exiting Class_2.__new__()"         return rv      def __init__(self, *a, **kw):         print "executing Class_2.__init__()"         super(Class_2, self).__init__(*a, **kw) 

Let's observe what happens when repeatedly trying to create an object of type Class_2

a = Class_2() # entering Meta_2.__call__() # entering Class_2.__new__() # exiting Class_2.__new__() # executing Class_2.__init__() # exiting Meta_2.__call__()  b = Class_2() # Class_2 singleton returning from Meta_2.__call__(), super(Meta_2, cls).__call__() skipped  c = Class_2() # Class_2 singleton returning from Meta_2.__call__(), super(Meta_2, cls).__call__() skipped  print a is b is c True 

Now observe this implementation using a class' __new__() method to try to accomplish the same thing.

import random class Class_3(object):      __Class_3_singleton__ = None      def __new__(cls, *a, **kw):         # if singleton not present create and save it         if not Class_3.__Class_3_singleton__:             print "entering Class_3.__new__()"             Class_3.__Class_3_singleton__ = rv = super(Class_3, cls).__new__(cls, *a, **kw)             rv.random1 = random.random()             rv.random2 = random.random()             print "exiting Class_3.__new__()"         else:             print ("Class_3 singleton returning from Class_3.__new__(), "                    "super(Class_3, cls).__new__() skipped")          return Class_3.__Class_3_singleton__       def __init__(self, *a, **kw):         print "executing Class_3.__init__()"         print "random1 is still {random1}".format(random1=self.random1)         # unfortunately if self.__init__() has some property altering actions         # they will affect our singleton each time we try to create an instance          self.random2 = random.random()         print "random2 is now {random2}".format(random2=self.random2)         super(Class_3, self).__init__(*a, **kw) 

Notice that the above implementation even though successfully registering a singleton on the class, does not prevent __init__() from being called, this happens implicitly in type.__call__() (type being the default metaclass if none is specified). This could lead to some undesired effects:

a = Class_3() # entering Class_3.__new__() # exiting Class_3.__new__() # executing Class_3.__init__() # random1 is still 0.282724600824 # random2 is now 0.739298365475  b = Class_3() # Class_3 singleton returning from Class_3.__new__(), super(Class_3, cls).__new__() skipped # executing Class_3.__init__() # random1 is still 0.282724600824 # random2 is now 0.247361634396  c = Class_3() # Class_3 singleton returning from Class_3.__new__(), super(Class_3, cls).__new__() skipped # executing Class_3.__init__() # random1 is still 0.282724600824 # random2 is now 0.436144427555  d = Class_3() # Class_3 singleton returning from Class_3.__new__(), super(Class_3, cls).__new__() skipped # executing Class_3.__init__() # random1 is still 0.282724600824 # random2 is now 0.167298405242  print a is b is c is d # True 
like image 31
Michael Ekoka Avatar answered Sep 25 '22 22:09

Michael Ekoka