【问题标题】:Python: How to create instance attribute by metaclass?Python:如何通过元类创建实例属性?
【发布时间】:2016-01-09 10:34:07
【问题描述】:

我正在使用元类为这样的新类创建属性:

class Property(object):
    def __init__(self, internal_name, type_, default_value):
        self._internal_name = internal_name
        self._type = type_
        self._default_value = default_value

    def generate_property(self):
        def getter(object_):
            return getattr(object_, self._internal_name)
        def setter(object_, value):
            if not isinstance(value, self._type):
                raise TypeError("Expect type {0}, got {1}.".format(self._type, type(value)))
            else:
                setattr(object_, self._internal_name, value)
        return property(getter, setter)

class AutoPropertyMeta(type):
    def __new__(cls, name, bases, attributes):
        for name, value in attributes.iteritems():
            if isinstance(value, Property):
                attributes[name] = value.generate_property()
        return super(AutoPropertyMeta, cls).__new__(cls, name, bases, attributes)

这样我就可以写出这样的代码了:

class SomeClassWithALotAttributes(object):
    __metaclass__ = AutoPropertyMeta
    attribute_a = Property("_attribute_a", int, 0)
    ...
    attribute_z = Property("_attribute_z", float, 1.0)

代替:

class SomeClassWithALotAttributes(object):
    def __init__(self):
        self._attribute_a = 0
        ...
        self._attribute_z = 1.0

    def get_attribute_a(self):
        return self._attribute_a

    def set_attribute_a(self, value):
        if not isinstance(value, int):
            raise TypeError("Expect type {0}, got {1}.".format(self._type, type(value))
        else:
            self._attribute_a = value

    attribute_a = property(get_attribute_a, set_attribute_a)
    ...

如果你总是在获取属性值之前设置值,它会很好用,因为 AutoPropertyMeta 只生成 gettersetter 方法。实际实例属性是在您第一次设置值时创建的。所以我想知道是否有办法通过元类为类创建实例属性。

这是我现在正在使用的一种解决方法,但我一直想知道是否有更好的方法:

class Property(object):
    def __init__(self, internal_name, type_, default_value):
        self._internal_name = internal_name
        self._type = type_
        self._default_value = default_value

    def generate_property(self):
        def getter(object_):
            return getattr(object_, self._internal_name)
        def setter(object_, value):
            if not isinstance(value, self._type):
                raise TypeError("Expect type {0}, got {1}.".format(self._type, type(value)))
            else:
                setattr(object_, self._internal_name, value)
        return property(getter, setter)

    def generate_attribute(self, object_):
        setattr(object_, self._internal_name, self._default_value)

class AutoPropertyMeta(type):
    def __new__(cls, name, bases, attributes):
        property_list = []
        for name, value in attributes.iteritems():
            if isinstance(value, Property):
                attributes[name] = value.generate_property()
                property_list.append(value)
        attributes["_property_list"] = property_list
        return super(AutoPropertyMeta, cls).__new__(cls, name, bases, attributes)

class AutoPropertyClass(object):
    __metaclass__ = AutoPropertyMeta
    def __init__(self):
        for property_ in self._property_list:
            property_.generate_attribute(self)

class SomeClassWithALotAttributes(AutoPropertyClass):
    attribute_a = Property("_attribute_a", int, 0)

【问题讨论】:

  • 不创建实例就无法创建实例属性。为什么不把代码放在 getter 中检查属性是否存在,如果不存在则返回默认值?
  • 你可以提前给属性赋值,不是吗?
  • @BrenBarn 如果属性不存在,我曾经尝试在 getter 中创建属性。但是通过这种方式,我必须在每次调用 getter 时检查属性是否存在。所以我改变了我在后面的问题中发布的解决方法。
  • 我不确定这是元类魔法的好用例。为什么你希望所有这些属性都是属性?真的只是为了强制进行类型检查吗?听起来你在这里有点与 python 战斗..
  • @kkpattern:这有什么大不了的?

标签: python metaclass


【解决方案1】:

下面是我所说的注入新__init__ 的示例。请注意,这只是为了好玩,您不应该这样做。

class Property(object):
    def __init__(self, type_, default_value):
        self._type = type_
        self._default_value = default_value

    def generate_property(self, name):
        self._internal_name = '_' + name
        def getter(object_):
            return getattr(object_, self._internal_name)
        def setter(object_, value):
            if not isinstance(value, self._type):
                raise TypeError("Expect type {0}, got {1}.".format(self._type, type(value)))
            else:
                setattr(object_, self._internal_name, value)
        return property(getter, setter)

class AutoPropertyMeta(type):
    def __new__(meta, name, bases, attributes):
        defaults = {}
        for name, value in attributes.iteritems():
            if isinstance(value, Property):
                attributes[name] = value.generate_property(name)
                defaults[name] = value._default_value
        # create __init__ to inject into the class
        # our __init__ sets up our secret attributes
        if '__init__' in attributes:
            realInit = attributes['__init__']
            # we do a deepcopy in case default is mutable
            # but beware, this might not always work
            def injectedInit(self, *args, **kwargs):
                for name, value in defaults.iteritems():
                    setattr(self, '_' + name, copy.deepcopy(value))
                # call the "real" __init__ that we hid with our injected one
                realInit(self, *args, **kwargs)
        else:
             def injectedInit(self, *args, **kwargs):
                for name, value in defaults.iteritems():
                    setattr(self, '_' + name, copy.deepcopy(value))
        # inject it
        attributes['__init__'] = injectedInit
        return super(AutoPropertyMeta, meta).__new__(meta, name, bases, attributes)

然后:

class SomeClassWithALotAttributes(object):
    __metaclass__ = AutoPropertyMeta
    attribute_a = Property(int, 0)
    attribute_z = Property(list, [1, 2, 3])

    def __init__(self):
        print("This __init__ is still called")

>>> x = SomeClassWithALotAttributes()
This __init__ is still called
>>> y = SomeClassWithALotAttributes()
This __init__ is still called
>>> x.attribute_a
0
>>> y.attribute_a
0
>>> x.attribute_a = 88
>>> x.attribute_a
88
>>> y.attribute_a
0
>>> x.attribute_z.append(88)
>>> x.attribute_z
[1, 2, 3, 88]
>>> y.attribute_z
[1, 2, 3]
>>> x.attribute_z = 88
Traceback (most recent call last):
  File "<pyshell#76>", line 1, in <module>
    x.attribute_z = 88
  File "<pyshell#41>", line 12, in setter
    raise TypeError("Expect type {0}, got {1}.".format(self._type, type(value)))
TypeError: Expect type <type 'list'>, got <type 'int'>.

这个想法是编写你自己的__init__ 来初始化秘密属性。然后,在创建类之前将其注入到类命名空间中,但存储对原始 __init__(如果有)的引用,以便在需要时调用它。

【讨论】:

  • 另外我想知道为什么不应该使用注入__init__技巧?(不要考虑深拷贝可变问题,我们可以通过默认值工厂而不是默认值来解决这个问题)。有什么危险?
  • @kkpattern:因为它会造成不必要的混乱,而且仅仅在 getter 内部进行检查并不能真正为您带来任何好处。此外,如果存在可能失败的边缘情况(例如,如果__init__ 中的某些子类调用super() 和/或具有此元类的类不在继承树的顶部),我也不会感到惊讶。
  • 在继承方面,注入__init__ 确实很危险。现在我认为在 getter 内部进行检查或在基类的帮助下创建实例属性是一种可接受的方式。但仍然很高兴学习注射技巧。我觉得接受这个答案还是可以的?
  • @kkpattern:当然。我之所以给出这个答案,是因为您在评论中说您很想学习一些 Python“技巧”! :-) 这种事情很高兴知道了解事情是如何运作的。在创建类时使用这种注入来修改它是有意义的,但你必须小心。但是注入 __init__ 特别危险,因为它是基本的并且经常被覆盖。
猜你喜欢
  • 1970-01-01
  • 2016-08-24
  • 1970-01-01
  • 2013-04-22
  • 2015-09-26
  • 2010-12-10
  • 2021-06-22
  • 2011-11-24
相关资源
最近更新 更多