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

Posted

技术标签:

【中文标题】如何向元类实例添加属性?【英文标题】:How to add properties to a metaclass instance? 【发布时间】:2015-02-22 03:52:02 【问题描述】:

我想定义元类,使我能够基于类的属性在新类中创建属性(即 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)

有时我会得到:

Name: Ketty; age: Ketty

有时:

Name: 22; age: 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 classes: Dynamic properties,但我想使用元类来完成。您是否知道如何做到这一点或有可能做到这一点?

【问题讨论】:

【参考方案1】:

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)

key 的值是在调用 fget 时确定的,而不是在定义 fget 时确定的。由于它是一个非局部变量,它的值可以在__new__ 函数的封闭范围内找到。当fget 被调用时,for-loop 已经结束,所以keylast 值就是找到的值。 Python3 的dict.item 方法以不可预知的顺序返回项目,所以有时最后一个键是__qualname__,这就是为什么有时会引发令人惊讶的错误,有时会返回相同的错误值对于所有没有错误的属性。


当您使用带默认值的参数定义函数时,默认值在定义函数时绑定到参数。 因此,当您将默认值绑定到 k 时,key 的当前值将更正绑定到 fgetfset

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


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

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 被调用时,key 的正确值会在make_fgetmake_fset 的封闭范围内找到。

【讨论】:

以上是关于如何向元类实例添加属性?的主要内容,如果未能解决你的问题,请参考以下文章

向元类添加方法

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

为啥不在实例属性查找中搜索元类的属性?

给定一个 Ruby 对象的实例,我如何获得它的元类?

学习日记0827异常处理 元类 自定义元类 自定义元类来实例化类 属性查找顺序

如何在 Groovy 类中“隐藏”元类属性