使用元类动态设置属性

Dynamically setting properties with metaclass

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

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

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

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

感谢任何帮助,谢谢。

问题是您有一个用于 getter 的 lambda 函数,因此 field_fn 的值在 getter for a 属性 首先被调用;因此,所有属性都将 return 为在上一次循环迭代中设置的局部变量 field_fn 的值,这解释了您注意到的奇怪行为。

您可以通过创建一个外部函数来解决这个问题,该函数将 属性 getter 包装在一个函数中,如下所示,并将参数传递给该函数,以便局部变量 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

或者,您也可以使用下面的 shorthand 而不是创建外部函数。这向 lambda 添加了一个新参数,它将 field_fn 绑定到 lambda 函数局部变量。由于用法是 属性 getter,例如 obj.a 调用,参数 fn 将使用在任何情况下设置的默认值,这是所需的这里的行为。

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

输出:

6 4