使用元类动态设置属性

Posted

技术标签:

【中文标题】使用元类动态设置属性【英文标题】:Dynamically setting properties with metaclass 【发布时间】:2022-01-02 23:31:03 【问题描述】:

我在创建一个元类时遇到了一个错误,该元类动态地创建具有给定配置文件的属性的类。

更详细地说,我希望动态创建具有某些属性的类(常规 setter 和在该属性上运行自定义函数结果的 getter)。所以我创建了一个工厂类来在运行时注册类和一个元类来设置给定配置字典的属性。

from copy import deepcopy
from typing import Any, Callable, Dict, Tuple


class Meta(type):
    def __new__(
        cls,
        clsname: str,
        bases: Tuple,
        attrs: Dict[str, Any],
        fields: Dict[str, Callable] = None,
    ) -> object:
        if fields is not None:
            for field, field_fn in fields.items():
                attrs[f"_field"] = None
                attrs[f"field"] = property(
                    fget=lambda self: deepcopy(field_fn)(getattr(self, f"_field")),
                    fset=lambda self, value: setattr(self, f"_field", value),
                )
        return super().__new__(cls, clsname, bases, attrs)


class Factory:
    registry = 

    @classmethod
    def register(
        cls,
        name: str,
        cfg: Dict[str, Callable],
    ) -> None:
        class ConfigurableClass(metaclass=Meta, fields=cfg):
            pass

        Factory.registry[name] = ConfigurableClass

    @classmethod
    def make(cls, name: str):
        return cls.registry[name]()


if __name__ == "__main__":
    Factory.register("foo", "a": lambda x: x + 1, "b": lambda x: x - 1)
    obj = Factory.make("foo")
    obj.a = 5
    obj.b = 5
    print(obj.a, obj.b)
    # Expected 6 and 4 but get 4 and 4 instead

但是,由于某种原因,最后一个 dict 键的功能被注册为 all 属性。我什至尝试在其中添加deepcopy

感谢任何帮助,谢谢。

【问题讨论】:

【参考方案1】:

问题在于您有一个用于 getter 的 lambda 函数,因此当第一次调用属性的 getter 时,field_fn 的值是惰性求值的;因此,所有属性都将返回在最后一次循环迭代中设置的本地 var field_fn 的值,这解释了您注意到的奇怪行为。

您可以通过创建一个将属性 getter 包装在如下所示的函数中的外部函数来解决此问题,并将参数传递给该函数,以便显式设置每个属性的本地 var field_fn。这样,每个属性都将使用显式绑定到它的本地 field_fn 值。

from typing import Any, Callable, Dict, Tuple


class Meta(type):
    def __new__(
        cls,
        clsname: str,
        bases: Tuple,
        attrs: Dict[str, Any],
        fields: Dict[str, Callable] = None,
    ) -> object:
        if fields is not None:
            for field, field_fn in fields.items():

                # the problem is that you have a lambda function, so the `field_fn`
                # is only evaluated when the `getter` is first called. Since the
                # `field_fn` is lazy evaluated in this way, the value of `field_fn`
                # from the last loop iteration is used instead. You can solve this
                # by creating a function that binds the local `field_fn` for each
                # iteration explicitly.
                def getter_for_field(field_fn: Callable):
                    # lambda function here will use `field_fn` in function
                    # locals, *not* the one which is set in each loop iteration.
                    return lambda self: field_fn(getattr(self, under_f))

                under_f = f'_field'

                attrs[under_f] = None
                attrs[field] = property(
                        ## Modified
                        fget=getter_for_field(field_fn),
                        ## End
                        fset=lambda self, value: setattr(self, under_f, value),
                    )

        return super().__new__(cls, clsname, bases, attrs)


class Factory:
    registry = 

    @classmethod
    def register(
        cls,
        name: str,
        cfg: Dict[str, Callable],
    ) -> None:
        class ConfigurableClass(metaclass=Meta, fields=cfg):
            pass

        Factory.registry[name] = ConfigurableClass

    @classmethod
    def make(cls, name: str):
        return cls.registry[name]()


if __name__ == "__main__":
    Factory.register("foo", "a": lambda x: x + 1, "b": lambda x: x - 1)
    obj = Factory.make("foo")
    obj.a = 5
    obj.b = 5
    print(obj.a, obj.b)
    # Expected 6 and 4 but get 4 and 4 instead

或者,您也可以使用下面的简写代替创建外部函数。这向 lambda 添加了一个新参数,它将field_fn 绑定到 lambda 函数局部变量。由于用法是作为属性获取器,例如像obj.a 那样调用,参数fn 将使用在任何情况下设置的默认值,这是此处所需的行为。

lambda self, fn=field_fn: fn(getattr(self, under_f))

输出:

6 4

【讨论】:

非常感谢!我知道发生了这样的事情,但我不知道为什么!我从未听说过“闭包”变量,我会阅读更多相关信息! @Eduardo 实际上,我想我自己有点困惑,所以我的信息可能有些错误。我更新了一个更好的解释,现在我什至更好地理解了这一点:-)

以上是关于使用元类动态设置属性的主要内容,如果未能解决你的问题,请参考以下文章

Grails 在对象引用上设置元类属性

python元类继承问题

Python 元类:为啥在类定义期间不调用 __setattr__ 来设置属性?

为啥 Pylint 在元类定义的属性使用上出错?

使用元类实现具有动态字段的描述符

元类编程--property动态属性