1

Okay so Im starting to mess around with voodoo and the darker side of Python with metaclasses, I wanted to get a simple example working before I modified it further, why is it not printing the print statement when I execute this code,

class Versioned(type):
    def __new__(cls, name, bases, dct):
        return type.__new__(cls, name, bases, dct)

    def __setattr__(cls, key, value):
        print "Setting attr %s to %s" % (key, value)
        return object.__setattr__(cls, key, value)

class Test(object):
    __metaclass__ = Versioned

    def __init__(self):
        self.a = 1
        self.b = 2

a = Test()
a.b = 31
print dir(a)
Jakob Bowyer
  • 33,878
  • 8
  • 76
  • 91

3 Answers3

2

The thing to understand is that a metaclass is not a class per se--it is a callable that returns a class--a class factory. It serves the same purpose as type(__name__, __bases__, __dict__).

>>> type('myclass', (), {})
<class '__main__.myclass'>

When you define __metaclass__, you are merely overriding the default class factory for that specific class or module. For example, this is a metaclass:

def setattr_logging_class(name, bases, dict_):
    """I am a metaclass"""
    def __setattr__(self, k, v):
        print "{} set attribute {} to {}".format(self, k, v)
        super(self.__class__, self).__setattr__(k, v)
    dict_['__setattr__'] = __setattr__
    cls = type(name, bases, dict_)
    return cls

class MyClass(object):
    __metaclass__ = setattr_logging_class
    def __init__(self):
        self.a = 1

obj = MyClass()
obj.b = 2

print obj.__dict__

Most importantly, a metaclass does not participate in the method resolution of the created class (unless you change bases). This is why your Versioned.__setattr__ is not visible to your Test instances. All Versioned did was return a new class (of type Versioned rather than type type) with the same name, bases and dict_ the Python runtime parsed out of your class Test(object): block.

Classes are themselves callable (via their __new__ method). (__new__ is for classes what __call__ is for instances.)

class MyClass(object):
    def __new__(cls_self, *args, **kwargs):
        print "I am {} called with {} and {}".format(cls_self, args, kwargs)
        return None

myobj = MyClass(1,2,3,a=4,b=5,c=6)
print myobj # == None

Since classes are callable, you can use a class as a metaclass. In fact, type is a class. Although, you can just as well use an instance with a __call__ method! These two examples are similar and there isn't anything you may want to do that can't be done in either way. (In fact, using an object is usually more straightforward.)

class MetaClass(type):
    def __new__(cls, name, bases, dict_):
        print "I am {} called with {}, {}, {}".format(cls, name, bases, dict_)
        return type.__new__(cls, name, bases, dict_)

class MetaObject(object):
    def __call__(self, name, bases, dict_):
        print "I am {} called with {}, {}, {}".format(self, name, bases, dict_)
        return type(name, bases, dict_)


class MyClass(object):
    __metaclass__ = MetaClass


class MyClass2(object):
    __metaclass__ = MetaObject()

Note that Python has many other metaprogramming methods besides metaclasses. In particular, Python >= 2.6 added support for class decorators, which covers most of the use cases for metaclasses with a much simpler interface. (Rather than create the class yourself, you get an already-created class object you modify.)

You can see a survey of multiple metaprogramming methods at this stackoverflow answer I recently wrote. The original question was "How do I make a built-in Python container type threadsafe?"

Community
  • 1
  • 1
Francis Avila
  • 31,233
  • 6
  • 58
  • 96
  • With a last name like 'Avila', how could you possibly have gone to school with the _Dominicans_? A true open mind you must have... – FastAl Feb 15 '16 at 23:36
1

I never really used metaclasses, but I think you're mixing inheritance with metaclasses. Metaclasses are there to build instances, but they don't add anything implicitly to your instance. There is no reason that the __setattr__ method should be imported in Test. To add it explicitely, you need to modify the Test dict (dct). Something like dct["__setattr__"] = cls.__setattr__. However, it does not work, and I don't understand why. The only way I found to make it work was :

class Versioned(type):
    def __new__(cls, name, bases, dct):
        print("BLAH")
        def __setattr__(cls, key, value):
            print("Setting attr %s to %s" % (key, value))
            return object.__setattr__(cls, key, value)
        dct["__setattr__"] = __setattr__
        return type.__new__(cls, name, bases, dct)

I hope this can help :-)

Finally, this link can help you understand metaclasses in python.

Community
  • 1
  • 1
Scharron
  • 17,233
  • 6
  • 44
  • 63
1

You are defining __setattr__ for the class itself, not for its instances. This will result in the print statement executing:

Test.key = value
Eric
  • 95,302
  • 53
  • 242
  • 374