39

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?

martineau
  • 119,623
  • 25
  • 170
  • 301
yarbelk
  • 7,215
  • 6
  • 29
  • 37
  • 1
    It's unclear what you are trying to achieve here, could you provide some information as to what is your end goal? I'm not sure you even need a `__metaclass__`. – Thomas Orozco Dec 07 '12 at 11:47
  • I can not understand why are you doing this. Maybe it would be better without a function, just metaclass? – alexvassel Dec 07 '12 at 11:47
  • 1
    I'm with them; typically this kind of problem is, to your credit relative to the multitude of questions here, you've actually over-thought the problem and devised a solution far more complex than is probably necessary. Spill the beans and get naked, what's on your mind? but i do like your question – John Mee Dec 07 '12 at 12:19
  • You say, "but this requires me to do.." I don't understand what use code you wanted to achieve. You want a class builder with arguments, and then you'll want to instantiate the class. This is exactly what you've shown. Show us what code you wanted to get to, it's not clear where you are headed. – Ned Batchelder Dec 07 '12 at 13:04

2 Answers2

63

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.

Passing Arguments to the Metaclass in Python 3.x

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.

UPDATE

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).

Breakdown

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".

martineau
  • 119,623
  • 25
  • 170
  • 301
John Crawford
  • 2,144
  • 2
  • 19
  • 16
  • 5
    This was extremely helpful. I wish you'd post a new question about this specifically about Python 3 and then answer it yourself - I had a hard time finding it. – Rick Nov 07 '14 at 12:23
  • http://cisco.safaribooksonline.com/9781449357337/datamodel_html?percentage=0&reader=html#X2ludGVybmFsX0h0bWxWaWV3P3htbGlkPTk3ODE0NDkzNTczMzclMkZfZGVmaW5pbmdfYV9tZXRhY2xhc3NfdGhhdF90YWtlc19vcHRpb25hbF9hcmd1bWVudHNfaHRtbCZxdWVyeT0= – Matthew Moisen Nov 16 '14 at 21:04
  • 1
    Done! Thank you for the suggestion! Sorry it took so long for me to see it!: http://stackoverflow.com/questions/27258557/metaclass-arguments-for-python-3-x/27258558#27258558 – John Crawford Dec 02 '14 at 20:44
  • 2
    @JohnCrawford, this is great work. I have not been able to wrap my head around this for months even going through the documentation thoroughly. Your examples need to be added to the official documentation. – kennes Apr 20 '18 at 19:28
  • 1
    Worth mentioning that you may add extra kwargs even if you're just inheriting a class that have already specified a custom metaclass. E.g. `class D(C, myArg2=2)` – Ivan Klass Oct 23 '18 at 19:58
  • 1
    Subtle point. When overriding `__init__()` and calling `super().__init__()` from it, it's a good idea to pass `**kwargs` to the latter, even though it will currently usually be empty so it can raise a `TypeError: __init_subclass__() takes no keyword arguments` whenever it's not. Also note this implies that any keyword arguments processed by the custom `__init__()` will need to be _removed_ from `kwargs` in order avoid passing them along. Done in this manner, the code should continue work even if `type.__init__()` later starts accepting some. – martineau Mar 01 '21 at 09:52
17

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.

martineau
  • 119,623
  • 25
  • 170
  • 301
  • See [this answer](http://stackoverflow.com/a/27259275/355230) for a version of the code that works in both Python 2 and 3. – martineau Dec 05 '14 at 15:23