1

TL;DR -
I have a class that uses a metaclass.
I would like to access the parameters of the object's constructor from the metaclass, just before the initialization process, but I couldn't find a way to access those parameters.
How can I access the constructor's parameters from the metaclass function __new__?


In order to practice the use of metaclasses in python, I would like to create a class that would be used as the supercomputer "Deep Thought" from the book "The Hitchhiker's Guide to the Galaxy".

The purpose of my class would be to store the various queries the supercomputer gets from users.
At the bottom line, it would just get some arguments and store them.
If one of the given arguments is number 42 or the string "The answer to life, the universe, and everything", I don't want to create a new object but rather return a pointer to an existing object.
The idea behind this is that those objects would be the exact same so when using the is operator to compare those two, the result would be true.

In order to be able to use the is operator and get True as an answer, I would need to make sure those variables point to the same object. So, in order to return a pointer to an existing object, I need to intervene in the middle of the initialization process of the object. I cannot check the given arguments at the constructor itself and modify the object's inner-variables accordingly because it would be too late: If I check the given parameters only as part of the __init__ function, those two objects would be allocated on different portions of the memory (they might be equal but won't return True when using the is operator).

I thought of doing something like that:

class SuperComputer(type):

    answer = 42

    def __new__(meta, name, bases, attributes):
        # Check if args contains the number "42" 
        # or has the string "The answer to life, the universe, and everything"
        # If so, just return a pointer to an existing object:
        return SuperComputer.answer

        # Else, just create the object as it is:
        return super(SuperComputer, meta).__new__(meta, name, bases, attributes)


class Query(object):
    __metaclass__ = SuperComputer

    def __init__(self, *args, **kwargs):
        self.args = args
        for key, value in kwargs.items():
            setattr(self, key, value)


def main():
    number = Query(42)
    string = Query("The answer to life, the universe, and everything")
    other = Query("Sunny", "Sunday", 123)
    num2 = Query(45)

    print number is string  # Should print True
    print other is string  # Should print False
    print number is num2  # Should print False


if __name__ == '__main__':
    main()

But I'm stuck on getting the parameters from the constructor.
I saw that the __new__ method gets only four arguments:
The metaclass instance itself, the name of the class, its bases, and its attributes.
How can I send the parameters from the constructor to the metaclass?
What can I do in order to achieve my goal?

Matan Itzhak
  • 2,702
  • 3
  • 16
  • 35
  • You're trying to do the check in the wrong place. It needs to be in `Query.__new__`, not in `SuperComputer.__new__`. You don't need a metaclass at all. – Aran-Fey Jan 14 '18 at 21:35
  • 1
    I don't see why you need a metaclass for this. The metaclass `__new__` is executed when you define the *class* `Query`, and it doesn't seem like you want to do anything at that time. If you want to check when Query is instantiated, just do the check in `Query.__new__`. – BrenBarn Jan 14 '18 at 21:36
  • So I don't need to use `metaclass` at all in this case? I thought I need to do so because this resembles a (semi-)singleton in the sense it sometimes returns the same object instead of creating a new one, and I saw the best way to achieve that is by using a metaclass (https://stackoverflow.com/a/6798042/6253504). Can you elaborate more so I would understand what's the difference and what can I do differently? – Matan Itzhak Jan 14 '18 at 21:43
  • using a metaclass is far from being the best way of creating a singleton. (I actually had mentioned that in my answer even before reading this comment) – jsbueno Jan 15 '18 at 03:39

1 Answers1

2

You don't need a metaclass for that.

The fact is __init__ is not the "constructor" of an object in Python, rather, it is commonly called an "initializator" . The __new__ is closer to the role of a "constructor" in other languages, and it is not available only for the metaclass - all classes have a __new__ method. If it is not explicitly implemented, the object.__new__ is called directly.

And actually, it is object.__new__ which creates a new object in Python. From pure Python code, there is no other possible way to create an object: it will always go through there. That means that if you implement the __new__ method on your own class, you have the option of not creating a new instance, and instead return another pre-existing instance of the same class (or any other object).

You only have to keep in mind that: if __new__ returns an instance of the same class, then the default behavior is that __init__ is called on the same instance. Otherwise, __init__ is not called.

It is also worth noting that in recent years some recipe for creating "singletons" in Python using metaclasses became popular - it is actually an overkill approach,a s overriding __new__ is also preferable for creating singletons.

In your case, you just need to have a dictionary with the parameters you want to track as your keys, and check if you create a new instance or "recycle" one whenever __new__ runs. The dictionary may be a class attribute, or a global variable at module level - that is your pick:

class Recycler:
   _instances = {}
   def __new__(cls, parameter1, ...):
       if parameter1 in cls._instances:
           return cls._instances[parameter1] 
       self = super().__new__(cls) # don't pass remaining parameters to object.__new__
       _instances[parameter1] = self
       return self

If you'd have any code in __init__ besides that, move it to __new__ as well.

You can have a baseclass with this behavior and have a class hierarchy without needing to re-implement __new__ for every class.

As for a metaclass, none of its methods are called when actually creating a new instance of the classes created with that metaclass. It would only be of use to automatically insert this behavior, by decorating or creating a fresh __new__ method, on classes created with that metaclass. Since this behavior is easier to track, maintain, and overall to combine with other classes just using ordinary inheritance, no need for a metaclass at all.

jsbueno
  • 99,910
  • 10
  • 151
  • 209