什么是DynamicClassAttribute,我该如何使用它?

32

从Python 3.4开始,有一个称为 DynamicClassAttribute 的描述符。文档中指出:

types.DynamicClassAttribute(fget=None, fset=None, fdel=None, doc=None)

将类上的属性访问路由到 __getattr__

这是一个描述符,用于定义通过实例和类访问时行为不同的属性。实例访问保持不变,但是通过类访问属性将被路由到类的 __getattr__ 方法;这是通过引发 AttributeError 来实现的。

这允许在实例上激活属性,并具有相同名称的类上虚拟属性(参见 Enum 的示例)。

自版本3.4起新增。

显然,它在枚举模块中使用。

# DynamicClassAttribute is used to provide access to the `name` and
# `value` properties of enum members while keeping some measure of
# protection from modification, while still allowing for an enumeration
# to have members named `name` and `value`.  This works because enumeration
# members are not set directly on the enum class -- __getattr__ is
# used to look them up.

@DynamicClassAttribute
def name(self):
    """The name of the Enum member."""
    return self._name_

@DynamicClassAttribute
def value(self):
    """The value of the Enum member."""
    return self._value_

我知道枚举类型有一些特殊之处,但我不明白这与DynamicClassAttribute有何关系。那些属性是什么意思,它们与正常属性有何不同,我该如何使用DynamicClassAttribute来获得优势?

2个回答

20

新版本:

我对之前的回答有些失望,所以我决定稍微重新写一下:

首先,看一下 DynamicClassAttribute 的源代码,你可能会注意到它看起来非常像普通的 property。除了 __get__ 方法外:

def __get__(self, instance, ownerclass=None):
    if instance is None:
        # Here is the difference, the normal property just does: return self
        if self.__isabstractmethod__:
            return self
        raise AttributeError()
    elif self.fget is None:
        raise AttributeError("unreadable attribute")
    return self.fget(instance)

这意味着如果要访问一个非抽象的DynamicClassAttribute类属性时,它会引发AttributeError而不是返回self。对于实例,if instance:的结果为True,且__get__property.__get__相同。

对于普通类而言,调用该属性时只会产生一个可见的AttributeError:

from types import DynamicClassAttribute
class Fun():
    @DynamicClassAttribute
    def has_fun(self):
        return False
Fun.has_fun

AttributeError - Traceback (most recent call last)

这本身并不是很有帮助,直到你查看使用metaclass时的"类属性查找"过程(我在这篇博客中找到了一张好看的图片)。因为如果一个属性引发了AttributeError并且该类具有元类, Python 就会查看metaclass.__getattr__方法,并查看是否可以解决该属性。通过以下最小示例来说明:

from types import DynamicClassAttribute

# Metaclass
class Funny(type):

    def __getattr__(self, value):
        print('search in meta')
        # Normally you would implement here some ifs/elifs or a lookup in a dictionary
        # but I'll just return the attribute
        return Funny.dynprop

    # Metaclasses dynprop:
    dynprop = 'Meta'

class Fun(metaclass=Funny):
    def __init__(self, value):
        self._dynprop = value

    @DynamicClassAttribute
    def dynprop(self):
        return self._dynprop

然后就到了“动态”的部分。如果您在类上调用 dynprop,它会搜索元数据并返回元数据的 dynprop

Fun.dynprop

打印出:

search in meta
'Meta'

因此,我们调用了 metaclass.__getattr__ 并返回原始属性(该属性与新属性具有相同的名称)。

对于实例,将返回 Fun 实例的 dynprop

Fun('Not-Meta').dynprop

我们获取了被覆盖的属性:

'Not-Meta'

我的结论是,如果您希望子类拥有与元类中使用的名称相同的属性,则DynamicClassAttribute非常重要。在实例上,它被屏蔽了,但如果在类上调用它,它仍然可以访问。

我也讨论了旧版本中Enum的行为,因此我将其保留在这里:

旧版本

如果您怀疑子类中设置的属性和基类上的属性可能存在命名冲突,则DynamicClassAttribute非常有用(对于这一点我不太确定)。

您需要至少了解一些关于元类的基础知识,因为如果不使用元类,这将无法工作(关于如何调用类属性的良好解释可以在此博客文章中找到),因为使用元类时属性查找略有不同。

假设您有:

class Funny(type):
    dynprop = 'Very important meta attribute, do not override'

class Fun(metaclass=Funny):
    def __init__(self, value):
        self._stub = value

    @property
    def dynprop(self):
        return 'Haha, overridden it with {}'.format(self._stub)

然后调用:

Fun.dynprop

0x1b3d9fd19a8所对应的属性

在实例上我们得到:

Fun(2).dynprop

'哈哈,用2覆盖了它'

糟糕...它已经丢失了。但是我们可以使用元类(metaclass)的特殊查找:让我们实现一个__getattr__(回退),并将dynprop实现为DynamicClassAttribute。因为根据文档来看,这就是它的目的——如果在类上调用,则会回退到__getattr__

from types import DynamicClassAttribute

class Funny(type):
    def __getattr__(self, value):
        print('search in meta')
        return Funny.dynprop

    dynprop = 'Meta'

class Fun(metaclass=Funny):
    def __init__(self, value):
        self._dynprop = value

    @DynamicClassAttribute
    def dynprop(self):
        return self._dynprop

现在我们访问类属性:

Fun.dynprop

输出:

search in meta
'Meta'
所以我们调用了 metaclass.__getattr__ 并返回原始属性(该属性与新属性使用相同的名称定义)。
对于实例:
Fun('Not-Meta').dynprop

我们获取被覆盖的属性:

'Not-Meta'

考虑到我们可以使用元类重新定义但被重写的属性来进行重定向而无需创建实例,因此情况并不太糟。这个示例是与Enum相反的操作,其中您在子类上定义属性:

from enum import Enum

class Fun(Enum):
    name = 'me'
    age = 28
    hair = 'brown'

并且希望默认情况下访问这些事后定义的属性。

Fun.name
# <Fun.name: 'me'>

但是你也想允许访问作为 DynamicClassAttribute 定义的 name 属性(返回变量实际具有的名称):

Fun('me').name
# 'name'
否则,你怎么能访问28的名称呢?
Fun.hair.age
# <Fun.age: 28>
# BUT:
Fun.hair.name
# returns 'hair'

看到区别了吗?为什么第二个不返回<Fun.name:'me'>呢?这是因为使用了DynamicClassAttribute。所以你可以遮盖原始属性,但稍后可以“释放”它。这种行为与我示例中显示的相反,并且至少需要使用__new____prepare__。但是,为此,您需要知道它的确切工作方式,并在许多博客和stackoverflow答案中得到解释,这些答案可以比我更好地解释,因此我将跳过深入探讨(而且我不确定我能否快速解决它)。

实际用例可能很少,但给定时间,人们可能会考虑一些……

DynamicClassAttribute文档上非常好的讨论:"我们添加它是因为我们需要它"


6

什么是DynamicClassAttribute

DynamicClassAttribute是一种类似于property的描述符。之所以加上Dynamic这个词,是因为无论您是通过类还是实例访问它,您都会得到不同的结果:

  • 实例访问与 property 相同,只需运行装饰的方法并返回其结果。

  • 类访问将引发 AttributeError; 当这种情况发生时,Python 会通过 mro 搜索每个父类,查找该属性 -- 如果没有找到,则调用类元类的 __getattr__ 来寻找属性。当然,__getattr__ 可以执行任何操作 -- 在 EnumMeta 的情况下,__getattr__ 查看类的 _member_map_ 是否存在所请求的属性,并在找到时返回它。顺便说一句:所有这些搜索都对性能产生了严重影响,这就是为什么我们最终将所有不具有名称冲突的成员放入 Enum 类的 __dict__ 中的原因。

那我该如何使用它?

您使用它的方式与使用property相同——唯一的区别是在创建其他枚举的基类时使用它。例如,来自aenum1Enum有三个保留名称:
  • name
  • value
  • values
values用于支持具有多个值的枚举成员。 该类有效地是:
class Enum(metaclass=EnumMeta):

    @DynamicClassAttribute
    def name(self):
        return self._name_

    @DynamicClassAttribute
    def value(self):
        return self._value_

    @DynamicClassAttribute
    def values(self):
        return self._values_

现在,任何 aenum.Enum 都可以拥有一个 values 成员,而不会破坏 Enum.<member>.values

1声明:我是Python标准库中Enum, enum34回溯,以及高级枚举(aenum)库的作者。


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