I'm trying to dynamically generate classes in python 2.7, and am wondering if you can easily pass arguments to the metaclass from the class object.
I've read this post, which is awesome, but doesn't quite answer the question. at the moment I am doing:
def class_factory(args, to, meta_class): Class MyMetaClass(type): def __new__(cls, class_name, parents, attrs): attrs['args'] = args attrs['to'] = to attrs['eggs'] = meta_class class MyClass(object): metaclass = MyMetaClass ...
but this requires me to do the following
MyClassClass = class_factory('spam', 'and', 'eggs') my_instance = MyClassClass()
Is there a cleaner way of doing this?
In Python, we can customize the class creation process by passing the metaclass keyword in the class definition. This can also be done by inheriting a class that has already passed in this keyword. We can see below that the type of MyMeta class is type and that the type of MyClass and MySubClass is MyMeta .
To create your own metaclass in Python you really just want to subclass type . A metaclass is most commonly used as a class-factory. When you create an object by calling the class, Python creates a new class (when it executes the 'class' statement) by calling the metaclass.
In order to set metaclass of a class, we use the __metaclass__ attribute.
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.
While the question is for Python 2.7 and already has an excellent answer, I had the same question for Python 3.3 and this thread was the closest thing to an answer I could find with Google. I found a better solution for Python 3.x by digging through the Python documentation, and I'm sharing my findings for anyone else coming here looking for a Python 3.x version.
After digging through Python's official documentation, I found that Python 3.x offers a native method of passing arguments to the metaclass, though not without its flaws.
Simply add additional keyword arguments to your class declaration:
class C(metaclass=MyMetaClass, myArg1=1, myArg2=2): pass
...and they get passed into your metaclass like so:
class MyMetaClass(type): @classmethod def __prepare__(metacls, name, bases, **kwargs): #kwargs = {"myArg1": 1, "myArg2": 2} return super().__prepare__(name, bases, **kwargs) def __new__(metacls, name, bases, namespace, **kwargs): #kwargs = {"myArg1": 1, "myArg2": 2} return super().__new__(metacls, name, bases, namespace) #DO NOT send "**kwargs" to "type.__new__". It won't catch them and #you'll get a "TypeError: type() takes 1 or 3 arguments" exception. def __init__(cls, name, bases, namespace, myArg1=7, **kwargs): #myArg1 = 1 #Included as an example of capturing metaclass args as positional args. #kwargs = {"myArg2": 2} super().__init__(name, bases, namespace) #DO NOT send "**kwargs" to "type.__init__" in Python 3.5 and older. You'll get a #"TypeError: type.__init__() takes no keyword arguments" exception.
You have to leave kwargs
out of the call to type.__new__
and type.__init__
(Python 3.5 and older; see "UPDATE") or will get you a TypeError
exception due to passing too many arguments. This means that--when passing in metaclass arguments in this manner--we always have to implement MyMetaClass.__new__
and MyMetaClass.__init__
to keep our custom keyword arguments from reaching the base class type.__new__
and type.__init__
methods. type.__prepare__
seems to handle the extra keyword arguments gracefully (hence why I pass them through in the example, just in case there's some functionality I don't know about that relies on **kwargs
), so defining type.__prepare__
is optional.
In Python 3.6, it appears type
was adjusted and type.__init__
can now handle extra keyword arguments gracefully. You'll still need to define type.__new__
(throws TypeError: __init_subclass__() takes no keyword arguments
exception).
In Python 3, you specify a metaclass via keyword argument rather than class attribute:
class MyClass(metaclass=MyMetaClass): pass
This statement roughly translates to:
MyClass = metaclass(name, bases, **kwargs)
...where metaclass
is the value for the "metaclass" argument you passed in, name
is the string name of your class ('MyClass'
), bases
is any base classes you passed in (a zero-length tuple ()
in this case), and kwargs
is any uncaptured keyword arguments (an empty dict
{}
in this case).
Breaking this down further, the statement roughly translates to:
namespace = metaclass.__prepare__(name, bases, **kwargs) #`metaclass` passed implicitly since it's a class method. MyClass = metaclass.__new__(metaclass, name, bases, namespace, **kwargs) metaclass.__init__(MyClass, name, bases, namespace, **kwargs)
...where kwargs
is always the dict
of uncaptured keyword arguments we passed in to the class definition.
Breaking down the example I gave above:
class C(metaclass=MyMetaClass, myArg1=1, myArg2=2): pass
...roughly translates to:
namespace = MyMetaClass.__prepare__('C', (), myArg1=1, myArg2=2) #namespace={'__module__': '__main__', '__qualname__': 'C'} C = MyMetaClass.__new__(MyMetaClass, 'C', (), namespace, myArg1=1, myArg2=2) MyMetaClass.__init__(C, 'C', (), namespace, myArg1=1, myArg2=2)
Most of this information came from Python's Documentation on "Customizing Class Creation".
Yes, there's an easy way to do it. In the metaclass's __new__()
method just check in the class dictionary passed as the last argument. Anything defined in the class
statement will be there. For example:
class MyMetaClass(type): def __new__(cls, class_name, parents, attrs): if 'meta_args' in attrs: meta_args = attrs['meta_args'] attrs['args'] = meta_args[0] attrs['to'] = meta_args[1] attrs['eggs'] = meta_args[2] del attrs['meta_args'] # clean up return type.__new__(cls, class_name, parents, attrs) class MyClass(object): __metaclass__ = MyMetaClass meta_args = ['spam', 'and', 'eggs'] myobject = MyClass() from pprint import pprint pprint(dir(myobject)) print myobject.args, myobject.to, myobject.eggs
Output:
['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__metaclass__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'args', 'eggs', 'to'] spam and eggs
Update
The code above will only work in Python 2 because syntax for specifying a metaclass was changed in an incompatible way in Python 3.
To make it work in Python 3 (but no longer in Python 2) is super simple to do and only requires changing the definition of MyClass
to:
class MyClass(metaclass=MyMetaClass): meta_args = ['spam', 'and', 'eggs']
It's also possible to workaround the syntax differences and produce code that works in both Python 2 and 3 by creating base classes "on-the-fly" which involves explicitly invoking the metaclass and using the class that is created as the base class of the one being defined.
class MyClass(MyMetaClass("NewBaseClass", (object,), {})): meta_args = ['spam', 'and', 'eggs']
Class construction in Python 3 has also been modified and support was added that allows other ways of passing arguments, and in some cases using them might be easier than the technique shown here. It all depends on what you're trying to accomplish.
See @John Crawford's detailed answer for a description of of the process in the new versions of Python.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With