How to create a metaclass that can give a class an

2019-05-31 08:03发布

问题:

I'm wondering how to create a metaclass in Python that can create other classes that:

  • Store their instances in an array automatically
  • Have a special instance, NonMetaClass.all, whose properties:
    • When set, set all the class's instances with the same key to the same value (e.g., Foo.all.num = 3 makes all instances of Foo have a num of 3)
    • When accessed (get), returns an array of all of the class's instances's key values (e.g., Foo.all.num returns [5, 3, 2])
    • Cannot be deleted.
    • When called (if the attribute is a function), call that method on all the instances of a class.

In Python terms, I would like to turn a class that is like this:

class Foo(object):
    BAR = 23
    def __init__(self):
        self.a = 5

    def pointless():
        print 'pointless.'

    def change_a(self):
        self.a = 52

Into this:

class Foo(object):
    BAR = 23
    instances = []
    all = # Some black magic to create the special "all" instance
    def __init__(self):
        self.a = 5
        Foo.instances.append(self)

    def pointless(self):
        print 'pointless.'

    def change_a(self):
        self.a = 52

And be able to use it like this:

>>> Foo()
>>> Foo.instances[0]
<__main__.Foo instance at 0x102ff5758>
>>> Foo()
>>> len(Foo.instances)
2
>>> Foo.all.a = 78
78
>>> Foo.all.a
[78, 78]
>>> Foo.all.change_a()
>>> Foo.all.a
[52, 52]
>>> 

回答1:

The only thing a metaclass is needed for there is actually quite easy: exactly creating the intances and all attributes.

All it have to do is to insert those into the namespace. Ah, it will also have to wrap the class __new__ method to insert new instances into the instances list.

The part that is the behavior wanted from all is interesting, and that can be implemented using the descriptor protocol, and attribute access control, so we have to craft a couple special classes, that will return the appropriate objects when requested after the ".".

"All" is the class that will be instantiated as "all" - it just needs a __get__ method to return another special object, from the AllAttr class, already bound to the parent class.

"AllAttr" is a special object that on any attribute access, perform your requirements on the members of the owner class "instance" attribute.

And "CallAllList" is a special list subclass that is callable, and calls all its members in turn. It is used by AllAttr if the required attribute from the owner class is callable itself.

class CallAllList(list):
    def __call__(self, *args, **kwargs):
        return [instance(*args, **kwargs) for instance in self]


class AllAttr(object):
    def __init__(self, owner):
        self._owner = owner

    def __getattr__(self, attr):
        method = getattr(self._owner, attr, None)
        cls = CallAllList if callable(method) else list
        return cls(getattr(obj, attr) for obj in self._owner.instances)

    def __setattr__(self, attr, value):
        if attr == "_owner":
            return super(AllAttr, self).__setattr__(attr, value)
        for obj in self._owner.instances:
            setattr(obj, attr, value)


class All(object):
    def __get__(self, instance, owner):
        return AllAttr(owner)

    def __repr__(self):
        return "Representation of all instances of '{}'".format(self.__class__.__name__)


class MetaAll(type):
    def __new__(metacls, name, bases, namespace):
        namespace["all"] = All()
        namespace["instances"] = []
        cls = super(MetaAll, metacls).__new__(metacls, name, bases, namespace)
        original_new = getattr(cls, "__new__")
        def __new__(cls, *args, **kwargs):
            instance = original_new(cls, *args, **kwargs)
            cls.instances.append(instance)
            return instance
        cls.__new__ = __new__
        return cls


class Foo(metaclass=MetaAll):
    pass

The code above is written so that it is Python 3 and Python 2 compatible, since you appear to still be using Python2 given your "print" example. The only thing that cannot be written compatible with both forms is the metaclass using declaration itself - just declare a __metaclass__ = MetaAll inside the body of your Foo class if you are using Python 2. But you should not really be using Python2, just change to Python 3 as soon as you can.

update

It happens that Python 2 has the "unbound method" figure, and the special casing of __new__ does not work like in Python 3: you can't just attribute a function named __new__ to the class. In order to get the correct __new__ method from the superclasses, the easiest way is to create a disposable class, so that it can be searched linearly. Otherwise, one would have to reimplement the MRO algorithm to get the proper __new__ method.

So, for Python 2, the metaclass should be this:

class MetaAll(type):
    def __new__(metacls, name, bases, namespace):
        namespace["all"] = All()
        namespace["instances"] = []
        if "__new__" in namespace:
            original_new = namespace["__new__"]
            def __new__(cls, *args, **kwargs):
                instance = original_new(cls, *args, **kwargs)
                cls.instances.append(instance)
                return instance
        else:
            # We create a disposable class just to get the '__mro__'
            stub_cls = super(MetaAll, metacls).__new__(metacls, name, bases, {})
            for parent in stub_cls.__mro__[1:]:
                if "__new__" in parent.__dict__:
                    original_new = parent.__dict__["__new__"]
                    break 

            def __new__(cls, *args, **kwargs):
                instance = original_new(cls, *args, **kwargs)
                cls.instances.append(instance)
                return instance
        namespace["__new__"] = __new__
        final_cls = super(MetaAll, metacls).__new__(metacls, name, bases, namespace)

        return final_cls


class Foo(object):
    __metaclass__ = MetaAll

(now, again, this thing is ancient. Just settle for Python 3.6)



回答2:

Ok, I figured out how to do this for Python 2.7 on my own. This is what I believe to be the best solution though it may not be the only one. It allows you to set, get, and function call on attributes of Class.all. I've named the metaclass InstanceUnifier, but please comment if you think there's a better (shorter, more descriptive) name you can think of.

class InstanceUnifier(type):
    '''
        What we want: A metaclass that can give a class an array of instances and provide a static Class.all object, that, when a method is called on it, calls the same method on every instance of the class.
    '''
    def __new__(cls, name, base_classes, dct):
        dct['all'] = None
        dct['instances'] = []
        return type.__new__(cls, name, base_classes, dct)
    def __init__(cls, name, base_classes, dct):
        class Accessor(object):
            def __getattribute__(self, name):
                array = [getattr(inst, name) for inst in cls.instances]
                if all([callable(item) for item in array]):
                    def proxy_func(*args, **kwargs):
                        for i in range(len(cls.instances)):
                            this = cls.instances[i]
                            func = array[i]
                            func(*args, **kwargs)
                    return proxy_func
                elif all([not callable(item) for item in array]):
                    return array
                else:
                    raise RuntimeError('Some objects in class instance array for key "'+name+'" are callable, some are not.')
            def __setattr__(self, name, value):
                [setattr(inst, name, value) for inst in cls.instances]
            def __delattr__(self, name):
                [delattr(inst, name) for inst in cls.instances]
        cls.all = Accessor()
        return type.__init__(cls, name, base_classes, dct)

    def __call__(cls, *args, **kwargs):
        inst = type.__call__(cls, *args, **kwargs)
        cls.instances.append(inst)
        return inst