如何向元类实例添加属性?

25

我希望定义一个元类,使我能够基于类的属性在新类中创建属性(例如setter、getter)。

例如,我想要定义以下类:

class Person(metaclass=MetaReadOnly):
    name = "Ketty"
    age = 22

    def __str__(self):
        return ("Name: " + str(self.name) + "; age: "
                + str(self.age))

但我希望得到类似这样的东西:

class Person():
    __name = "Ketty"
    __age = 22

    @property
    def name(self):
        return self.__name;
    @name.setter
    def name(self, value):
        raise RuntimeError("Read only")

    @property
    def age(self):
        return self.__age
    @age.setter
    def age(self, value):
        raise RuntimeError("Read only")

    def __str__(self):
        return ("Name: " + str(self.name) + "; age: "
                + str(self.age))

这是我编写的元类:

class MetaReadOnly(type):
    def __new__(cls, clsname, bases, dct):

        result_dct = {}

        for key, value in dct.items():
            if not key.startswith("__"):
                result_dct["__" + key] = value

                fget = lambda self: getattr(self, "__%s" % key)
                fset = lambda self, value: setattr(self, "__"
                                                   + key, value)

                result_dct[key] = property(fget, fset)

            else:
                result_dct[key] = value

        inst = super(MetaReadOnly, cls).__new__(cls, clsname,
                                                bases, result_dct)

        return inst

    def raiseerror(self, attribute):
        raise RuntimeError("%s is read only." % attribute)

然而它不能正常运行。

client = Person()
print(client)

有时我会看到:

姓名:Ketty;年龄:Ketty

有时候是:

姓名:22;年龄:22

甚至还可能出现错误:

Traceback (most recent call last):
  File "F:\Projects\TestP\src\main.py", line 38, in <module>
    print(client)
  File "F:\Projects\TestP\src\main.py", line 34, in __str__
    return ("Name: " + str(self.name) + "; age: " + str(self.age))
  File "F:\Projects\TestP\src\main.py", line 13, in <lambda>
    fget = lambda self: getattr(self, "__%s" % key)
AttributeError: 'Person' object has no attribute '____qualname__'

我找到了一个例子,它展示了另一种实现方式:Python类:动态属性,但我想使用元类来实现。你有任何想法吗?这是否可能实现?

2个回答

28

key当前值绑定到您的fgetfset定义中的参数:

fget = lambda self, k=key: getattr(self, "__%s" % k)
fset = lambda self, value, k=key: setattr(self, "__" + k, value)
这是一个典型的 Python 陷阱。当你定义一个

fget = lambda self: getattr(self, "__%s" % key)

fget 被调用时决定了 key 的值,而不是在定义 fget 时确定。由于它是一个nonlocal变量,其值可以在 __new__ 函数的封闭范围内找到。当 fget 被调用时,for-loop 已经结束,因此 key 的最后一个值就是被找到的值。Python3 的 dict.item 方法以无序的方式返回items,因此有时最后的key是,例如,__qualname__,这就是为什么有时会引发意外错误,有时对所有属性返回相同的错误值。


当你使用默认值参数定义函数时,默认值将绑定到参数上,但实际上是在函数定义时进行绑定。因此,当你将默认值绑定到 k 上时,当前的 key 值被正确地绑定到了 fgetfset 上。

与之前不同,k 现在是一个局部变量。默认值存储在 fget.__defaults__fset.__defaults__ 中。


另一种选择是使用闭包。你可以在元类之外定义它:

def make_fget(key):
    def fget(self):
        return getattr(self, "__%s" % key)
    return fget
def make_fset(key):
    def fset(self, value):
        setattr(self, "__" + key, value)
    return fset

并在元类中像这样使用:

result_dct[key] = property(make_fget(key), make_fset(key))

现在当调用fgetfset时,在make_fgetmake_fset的上层作用域中找到key的正确值。


0

@unutbu的回答非常好,很好地解决了主要注意事项。

但是,还有一件事情需要注意(我还不能发表评论,所以在这里提出整个答案),请注意classpropertyinstanceproperty具有不同的键。

您在这里做的一切都完全基于类属性。如果希望为“实例”属性设置setter,则键必须为f"_{clsname}__{key}"。我缺乏Python数据模型的技术理解来解释我所说的是否完全正确或是否存在其他注意事项。只是作为附注在此发布。

如果您希望允许用户使用自定义方法覆盖方法,例如我在此处提供的示例(即使它没有语法错误,我也不确定,但仍然在此发布):

class MyMeta(type):
    def __new__(cls, clsname, bases, info, **kwargs):
        property_key = "__my_private"  # should have been f"_{clsname}__my_private"
        info[property_key] = "default_value"
        if "my_getter" not in info:
            info["my_getter"] = lambda self, key=property_key: getattr(self, key)
        return super().__new__(cls, clsname, bases, info, **kwargs)

class MyClass(metaclass=MyMeta):
    def my_getter(self):
        return f"will fail to get {self.__my_private}"

MyClass().my_getter() # => Raises: AttributeError "_MyClass__my_private" not found.
# This would work so fine if no `self` keyword was being used (treated as classproperty)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接