Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What's the correct way to implement a metaclass with a different signature than `type`?

Say I want to implement a metaclass that should serve as a class factory. But unlike the type constructor, which takes 3 arguments, my metaclass should be callable without any arguments:

Cls1 = MyMeta()
Cls2 = MyMeta()
...

For this purpose I defined a custom __new__ method with no parameters:

class MyMeta(type):
    def __new__(cls):
        return super().__new__(cls, 'MyCls', (), {})

But the problem is that python automatically calls the __init__ method with the same arguments as the __new__ method, so trying to call MyMeta() ends up throwing an exception:

TypeError: type.__init__() takes 1 or 3 arguments

Which makes sense, since type can be called with 1 or 3 arguments. But what's the correct way to fix this? I see 3 (4?) options:

  • I could add an empty __init__ method to my metaclass, but since I'm not sure if type.__init__ does anything important, this might not be a good idea.
  • I could implement an __init__ method that calls super().__init__(cls.__name__, cls.__bases__, vars(cls)).
  • I could use a meta-metaclass and override its __call__ method, rather than messing with __new__ and __init__.
  • Bonus option: Maybe I shouldn't try to change the signature?

So my question is: Are the 3 solutions I listed correct or are there any subtle bugs hidden in them? Which solution is best (i.e. the most correct)?

like image 246
Aran-Fey Avatar asked Sep 05 '18 13:09

Aran-Fey


1 Answers

An interface deviating from the parent signature is a questionable design in regular classes too. You don't need the extra complexity of metaclasses to get into this kind of mess - you can cause the same new/init jumble by subclassing a datetime or whatever.

I want to have a metaclass and an easy way to create instances of that metaclass.

The usual pattern in Python is to write a factory using a from_something classmethod. To take the example of creating datetime instances from a different init signature, there is for example datetime.fromtimestamp, but you have many other examples too (dict.fromkeys, int.from_bytes, bytes.fromhex...)

There is nothing specific to metaclasses here, so use the same pattern:

class MyMeta(type):
    @classmethod
    def from_no_args(cls, name=None):
        if name is None:
            name = cls.__name__ + 'Instance'
        return cls(name, (), {})

Usage:

>>> class A(metaclass=MyMeta):
...     pass
... 
>>> B = MyMeta.from_no_args()
>>> C = MyMeta.from_no_args(name='C')
>>> A.__name__
'A'
>>> B.__name__
'MyMetaInstance'
>>> C.__name__
'C'
like image 137
wim Avatar answered Nov 14 '22 17:11

wim