如何创建可以为类提供实例数组并提供作用于所有类实例的“巫毒”实例的元类?

Posted

技术标签:

【中文标题】如何创建可以为类提供实例数组并提供作用于所有类实例的“巫毒”实例的元类?【英文标题】:How to create a metaclass that can give a class an array of instances and provide a "voodoo" instance that acts on all class instances? 【发布时间】:2018-05-07 15:03:09 【问题描述】:

我想知道如何在 Python 中创建一个可以创建其他类的元类:

自动将它们的实例存储在数组中 有一个特殊的实例NonMetaClass.all,它的属性: 设置后,将具有相同键的类的所有实例设置为相同的值(例如,Foo.all.num = 3 使 Foo 的所有实例具有 3 个 num) 访问 (get) 时,返回一个包含所有类实例键值的数组(例如,Foo.all.num 返回 [5, 3, 2]) 无法删除。 调用时(如果属性是函数),在类的所有实例上调用该方法。

用 Python 的话,我想转一个这样的类:

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

    def pointless():
        print 'pointless.'

    def change_a(self):
        self.a = 52

进入这个:

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

并且能够像这样使用它:

>>> 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]
>>> 

【问题讨论】:

关心填写子类化“Foo”时应该如何表现?子类也应该是 Foo 的实例的一部分,还是每个子类都需要一个空的实例数组? 所以,最简单的事情是每个子类都有自己的“instances”属性,这就是我发布的答案所做的。 是的,这就是我想要的行为,谢谢。一旦我可以使用计算机(我现在正在使用我的手机)测试程序,我就会投票并接受你的回答。 【参考方案1】:

唯一需要元类的东西实际上很简单: 完全创建 intancesall 属性。

它所要做的就是将它们插入到命名空间中。啊,它还必须包装类 __new__ 方法以将新实例插入到 instances 列表中。

all 想要的行为部分很有趣,可以使用描述符协议和属性访问控制来实现,因此我们必须制作几个特殊的类,它们会在请求时返回适当的对象之后 ”。”。

“All”是将被实例化为“all”的类 - 它只需要一个 __get__ 方法来返回另一个特殊对象,该对象来自已绑定到父类的 AllAttr 类。

“AllAttr”是一个特殊对象,在任何属性访问时,都对所有者类“instance”属性的成员执行您的要求。

而“CallAllList”是一个特殊的可调用列表子类,依次调用其所有成员。如果所有者类的所需属性本身是可调用的,则 AllAttr 使用它。

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

上面的代码是为了兼容 Python 3 和 Python 2 而编写的,因为鉴于您的“打印”示例,您似乎仍在使用 Python2。 唯一不能与这两种形式兼容的是元类 using 声明本身 - 如果您使用 Python 2,只需在 Foo 类的主体内声明 __metaclass__ = MetaAll 即可。但您不应该真正使用 Python2,只需更改为尽快使用 Python 3。

更新

碰巧 Python 2 有“未绑定方法”图,__new__ 的特殊大小写不像在 Python 3 中那样工作:你不能只将名为 __new__ 的函数归因于类。为了从超类中获得正确的__new__ 方法,最简单的方法是创建一个一次性类,以便对其进行线性搜索。否则,必须重新实现 MRO 算法才能获得正确的 __new__ 方法。

因此,对于 Python 2,元类应该是这样的:

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

(现在,再一次,这件事很古老。只要满足于 Python 3.6)

【讨论】:

你测试你的代码了吗?我刚刚做了并得到了错误TypeError: unbound method __new__() must be called with Foo instance as first argument (got MetaAll instance instead) 我在 Python 3 中对其进行了测试。Python 3 没有“未绑定方法”之类的东西——这是 Python 2 的东西。那么-您可以更改为python 3吗?这将需要一些更改,因为 Python 2 无法将上面准备的 __new__ 识别为特例类方法。 感谢编辑!我很快就会离开我的手机,回到一台普通的(安装了 python 的)电脑上再次测试这个。【参考方案2】:

好的,我自己想出了如何为 Python 2.7 执行此操作。这是我认为最好的解决方案,尽管它可能不是唯一的。它允许您对Class.all 的属性进行设置、获取和函数调用。我已将元类命名为 InstanceUnifier,但如果您认为有更好(更短、更具描述性)的名称,请发表评论。

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

【讨论】:

以上是关于如何创建可以为类提供实例数组并提供作用于所有类实例的“巫毒”实例的元类?的主要内容,如果未能解决你的问题,请参考以下文章

python类和实例化

设计模式-单例模式(Singleton Pattren)

设计模式之单例模式

当我为类创建新实例时在 Swift 中获取 nil 值

设计模式—单例模式

python之元编程(元类实例)