Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Setting a class' metaclass using a decorator

Following this answer it seems that a class' metaclass may be changed after the class has been defined by using the following*:

class MyMetaClass(type):
    # Metaclass magic...

class A(object):
    pass

A = MyMetaClass(A.__name__, A.__bases__, dict(A.__dict__))

Defining a function

def metaclass_wrapper(cls):
    return MyMetaClass(cls.__name__, cls.__bases__, dict(cls.__dict__))

allows me to apply a decorator to a class definition like so,

@metaclass_wrapper
class B(object):
    pass

It seems that the metaclass magic is applied to B, however B has no __metaclass__ attribute. Is the above method a sensible way to apply metaclasses to class definitions, even though I am definiting and re-definiting a class, or would I be better off simply writing

class B(object):
    __metaclass__ = MyMetaClass
    pass

I presume there are some differences between the two methods.


*Note, the original answer in the linked question, MyMetaClass(A.__name__, A.__bases__, A.__dict__), returns a TypeError:

TypeError: type() argument 3 must be a dict, not dict_proxy

It seems that the __dict__ attribute of A (the class definition) has a type dict_proxy, whereas the type of the __dict__ attribute of an instance of A has a type dict. Why is this? Is this a Python 2.x vs. 3.x difference?

like image 768
Chris Avatar asked Jun 18 '12 21:06

Chris


3 Answers

Admittedly, I am a bit late to the party. However, I fell this was worth adding.

This is completely doable. That being said, there are plenty of other ways to accomplish the same goal. However, the decoration solution, in particular, allows for delayed evaluation ( obj = dec(obj) ), which using __metaclass__ inside the class does not. In typical decorator style, my solution is below.

There is a tricky thing that you may run into if you just construct the class without changing the dictionary or copying its attributes. Any attributes that the class had previously (before decorating) will appear to be missing. So, it is absolutely essential to copy these over and then tweak them as I have in my solution.

Personally, I like to be able to keep track of how an object was wrapped. So, I added the __wrapped__ attribute, which is not strictly necessary. It also makes it more like functools.wraps in Python 3 for classes. However, it can be helpful with introspection. Also, __metaclass__ is added to act more like the normal metaclass use case.

def metaclass(meta):
    def metaclass_wrapper(cls):
        __name = str(cls.__name__)
        __bases = tuple(cls.__bases__)
        __dict = dict(cls.__dict__)

        for each_slot in __dict.get("__slots__", tuple()):
            __dict.pop(each_slot, None)

        __dict["__metaclass__"] = meta

        __dict["__wrapped__"] = cls

        return(meta(__name, __bases, __dict))
    return(metaclass_wrapper)

For a trivial example, take the following.

class MetaStaticVariablePassed(type):
    def __new__(meta, name, bases, dct):
        dct["passed"] = True

        return(super(MetaStaticVariablePassed, meta).__new__(meta, name, bases, dct))

@metaclass(MetaStaticVariablePassed)
class Test(object):
    pass

This yields the nice result...

|1> Test.passed
|.> True

Using the decorator in the less usual, but identical way...

class Test(object):
    pass

Test = metaclass_wrapper(Test)

...yields, as expected, the same nice result.

|1> Test.passed
|.> True
like image 105
jakirkham Avatar answered Nov 02 '22 02:11

jakirkham


My summary of your question: "I tried a new tricky way to do a thing, and it didn't quite work. Should I use the simple way instead?"

Yes, you should do it the simple way. You haven't said why you're interested in inventing a new way to do it.

like image 27
Ned Batchelder Avatar answered Nov 02 '22 01:11

Ned Batchelder


The class has no __metaclass__ attribute set... because you never set it!

Which metaclass to use is normally determined by a name __metaclass__ set in a class block. The __metaclass__ attribute isn't set by the metaclass. So if you invoke a metaclass directly rather than setting __metaclass__ and letting Python figure it out, then no __metaclass__ attribute is set.

In fact, normal classes are all instances of the metaclass type, so if the metaclass always set the __metaclass__ attribute on its instances then every class would have a __metaclass__ attribute (most of them set to type).


I would not use your decorator approach. It obscures the fact that a metaclass is involved (and which one), is still one line of boilerplate, and it's just messy to create a class from the 3 defining features of (name, bases, attributes) only to pull those 3 bits back out from the resulting class, throw the class away, and make a new class from those same 3 bits!

When you do this in Python 2.x:

class A(object):
    __metaclass__ = MyMeta
    def __init__(self):
        pass

You'd get roughly the same result if you'd written this:

attrs = {}
attrs['__metaclass__'] = MyMeta
def __init__(self):
    pass
attrs['__init__'] = __init__
A = attrs.get('__metaclass__', type)('A', (object,), attrs)

In reality calculating the metaclass is more complicated, as there actually has to be a search through all the bases to determine whether there's a metaclass conflict, and if one of the bases doesn't have type as its metaclass and attrs doesn't contain __metaclass__ then the default metaclass is the ancestor's metaclass rather than type. This is one situation where I expect your decorator "solution" will differ from using __metaclass__ directly. I'm not sure exactly what would happen if you used your decorator in a situation where using __metaclass__ would give you a metaclass conflict error, but I wouldn't expect it to be pleasant.

Also, if there are any other metaclasses involved, your method would result in them running first (possibly modifying what the name, bases, and attributes are!) and then pulling those out of the class and using it to create a new class. This could potentially be quite different than what you'd get using __metaclass__.

As for the __dict__ not giving you a real dictionary, that's just an implementation detail; I would guess for performance reasons. I doubt there is any spec that says the __dict__ of a (non-class) instance has to be the same type as the __dict__ of a class (which is also an instance btw; just an instance of a metaclass). The __dict__ attribute of a class is a "dictproxy", which allows you to look up attribute keys as if it were a dict but still isn't a dict. type is picky about the type of its third argument; it wants a real dict, not just a "dict-like" object (shame on it for spoiling duck-typing). It's not a 2.x vs 3.x thing; Python 3 behaves the same way, although it gives you a nicer string representation of the dictproxy. Python 2.4 (which is the oldest 2.x I have readily available) also has dictproxy objects for class __dict__ objects.

like image 27
Ben Avatar answered Nov 02 '22 03:11

Ben