没有任何描述符魔法的Python属性查找?

7 投票
4 回答
4110 浏览
提问于 2025-04-15 15:24

我最近在写代码时,开始更频繁地使用Python的描述符协议。通常,我希望Python默认的查找方式能够正常工作,但有时候我发现我想直接获取描述符对象本身,而不是它的__get__方法返回的结果。比如,我想知道描述符的类型,或者访问存储在描述符中的状态,或者其他类似的事情。

我写了下面的代码,按照我认为正确的顺序遍历命名空间,并返回属性的原始值,无论它是否是描述符。不过,我很惊讶的是,我找不到一个内置函数或者标准库中的东西来做到这一点——我觉得应该有,只是我没注意到或者没有搜索到正确的关键词。

在Python的发行版中,有没有已经实现这种功能(或类似功能)的东西呢?

谢谢!

from inspect import isdatadescriptor

def namespaces(obj):
    obj_dict = None
    if hasattr(obj, '__dict__'):
        obj_dict = object.__getattribute__(obj, '__dict__')

    obj_class = type(obj)
    return obj_dict, [t.__dict__ for t in obj_class.__mro__]

def getattr_raw(obj, name):
    # get an attribute in the same resolution order one would normally,
    # but do not call __get__ on the attribute even if it has one
    obj_dict, class_dicts = namespaces(obj)

    # look for a data descriptor in class hierarchy; it takes priority over
    # the obj's dict if it exists
    for d in class_dicts:
        if name in d and isdatadescriptor(d[name]):
            return d[name]

    # look for the attribute in the object's dictionary
    if obj_dict and name in obj_dict:
        return obj_dict[name]

    # look for the attribute anywhere in the class hierarchy
    for d in class_dicts:
        if name in d:
            return d[name]

    raise AttributeError

编辑:2009年10月28日,星期三。

Denis的回答给了我一个在我的描述符类中使用的约定,以便获取描述符对象本身。但是,我有一个完整的描述符类层次结构,我不想让每一个__get__函数都写一些重复的代码。

def __get__(self, instance, instance_type):
    if instance is None: 
        return self
    ...

为了避免这种情况,我让描述符类树的根类继承以下内容:

def decorate_get(original_get):
    def decorated_get(self, instance, instance_type):
        if instance is None:
            return self
        return original_get(self, instance, instance_type)
    return decorated_get

class InstanceOnlyDescriptor(object):
    """All __get__ functions are automatically wrapped with a decorator which
    causes them to only be applied to instances. If __get__ is called on a 
    class, the decorator returns the descriptor itself, and the decorated
    __get__ is not called.
    """
    class __metaclass__(type):
        def __new__(cls, name, bases, attrs):
            if '__get__' in attrs:
                attrs['__get__'] = decorate_get(attrs['__get__'])
            return type.__new__(cls, name, bases, attrs)

4 个回答

0

上面的方法

class FixedValueProperty(object):
    def __init__(self, value):
        self.value = value
    def __get__(self, inst, cls):
        if inst is None:
            return self
        return self.value

在你能控制属性代码的时候是个很好的方法,但有些情况下,比如当属性是别人控制的库的一部分时,另一种方法会更有用。这种替代方法在其他情况下也很有用,比如实现对象映射、遍历命名空间(就像问题中提到的那样),或者其他一些特殊的库。

想象一下一个简单属性的类:

class ClassWithProp:

    @property
    def value(self):
        return 3
>>>test=ClassWithProp()
>>>test.value
3
>>>test.__class__.__dict__.['value']
<property object at 0x00000216A39D0778>

当从容器对象的类dict访问时,'描述符魔法'就被绕过了。还要注意,如果我们把属性赋值给一个新的类变量,它的行为和原来的属性一样,依然有'描述符魔法',但如果赋值给实例变量,属性就像任何普通对象一样,绕过了'描述符魔法'。

>>> test.__class__.classvar =  test.__class__.__dict__['value']
>>> test.classvar
3
>>> test.instvar = test.__class__.__dict__['value']
>>> test.instvar
<property object at 0x00000216A39D0778>
3

inspect库提供了一个函数,可以在没有任何特殊处理的情况下获取属性,这个函数叫做 inspect.getattr_static

文档链接: https://docs.python.org/3/library/inspect.html#fetching-attributes-statically

(这个问题比较老,但我每次想起怎么做的时候都会遇到,所以我发这个答案,以便我能再次找到它!)

13

大多数描述符在作为实例属性访问时才能正常工作。所以,当你以类的方式访问它时,返回它自己是很方便的:

class FixedValueProperty(object):
    def __init__(self, value):
        self.value = value
    def __get__(self, inst, cls):
        if inst is None:
            return self
        return self.value

这样你就可以直接获取到描述符本身:

>>> class C(object):
...     prop = FixedValueProperty('abc')
... 
>>> o = C()
>>> o.prop
'abc'
>>> C.prop
<__main__.FixedValueProperty object at 0xb7eb290c>
>>> C.prop.value
'abc'
>>> type(o).prop.value
'abc'

注意,这种方式对大多数内置描述符也适用:

>>> class C(object):
...     @property
...     def prop(self):
...         return 'abc'
... 
>>> C.prop
<property object at 0xb7eb0b6c>
>>> C.prop.fget
<function prop at 0xb7ea36f4>

当你需要在子类中扩展描述符时,访问描述符可能会很有用,但其实有一种更好的方法来实现这一点。

撰写回答