【问题标题】:Why attribute lookup in Python is designed this way (precedence chain)?为什么 Python 中的属性查找是这样设计的(优先链)?
【发布时间】:2016-05-04 00:57:16
【问题描述】:

我刚刚接触了 Python 中的描述符,并在“__get__、__set__、__delete__”上获得了有关描述符协议的想法,它在包装方法方面确实做得很好。

不过,the protocol 中还有其他规则:

数据和非数据描述符的不同之处在于针对实例字典中的条目计算覆盖的方式。如果实例的字典中有一个与数据描述符同名的条目,则数据描述符优先。如果实例的字典中有一个与非数据描述符同名的条目,则字典条目优先。

我没明白,用经典的方式查找不好吗(实例字典 -> 类字典 -> 基类字典)?
如果这样实现,数据描述符可以由实例持有,而描述符本身不必持有weakrefdict来保存所有者不同实例的值。
为什么要将描述符放入查找链中?又为什么把数据描述符放在最开始?

【问题讨论】:

  • 在查看了所有答案并与助手沟通后,我得到了这里的逻辑:描述符是为 OOP 设计的,它用于描述类的属性(数据属性和方法)的行为,那就是为什么这个协议被称为“描述符”。所以描述符协议在类级别工作。对于数据描述符,它们描述了数据属性,为了实现这一点,查找优先链必须采用这种方式。所以,这个描述符协议都是关于 OOP 的。这个逻辑对我来说很有意义。感谢 skyking、Nikita 和所有其他帮助者。

标签: python descriptor


【解决方案1】:

首先,经典的方式(实际上并没有太大变化)不是你描述的那样。从这个意义上说,实际上没有基类,基类只是在类创建期间使用的东西。经典查找是先在实例中查找,然后在类中查找。

引入描述符的原因是为了提供一种更简洁的方式来自定义属性访问。经典方式依赖于可查找的函数来设置和获取属性。新方法还允许使用 @property 装饰器定义属性。

现在出于区分数据和非数据(或 RW 和 RO)描述符的原因。第一个应该注意的是,无论您尝试什么类型的访问(无论是读取、写入还是删除),进行相同的查找都是合理的:

描述符应该优先于 RO 描述符的原因是,如果您有 RO 描述符,您的意图通常是属性应该是只读的。这意味着在这种情况下使用描述符是正确的。

另一方面,如果您有 RW 描述符,则使用 __dict__ 条目来存储实际数据会很有用。

还应注意,描述符正确地放置在类中而不是实例中(如果使用该方法找到对象,则属性查找会自动调用__get__)。

为什么不是另一种方式是因为如果您将描述符放在实例中,您可能希望该属性实际引用描述符,而不是描述符会让您认为它是什么(通过调用__get__) .例如:

class D:
    def __get__(self):
        return None

class C:
    pass

o = C()
d = D()

o.fubar = d

现在最后一条语句可能是我们实际上将D() 存储在o.fubar 中,目的是让o.fubar 返回d,而不是调用d.__get__(),后者将返回None

【讨论】:

  • 我真的不明白你的意思。现在让我困惑的是:为什么 b.x 被转换成type(b).__dict__['x'].__get__(b, type(b)),而不是b.__dict__['x'].__get__(b, type(b))。为什么所有的描述符几乎都用在类级别,而不是实例级别?
  • @hisen 这是因为如果在实例的__dict__ 中找到描述符,则意味着相应的属性实际上是指一个描述符,而不是调用__get__ 时所得到的。因此,如果您希望描述符被自动调用,它必须在类中。
  • 是的,我知道,但我想知道为什么它是这样设计的。为什么不按照旧的查找方式,当找到一个属性时,检查它是否有 __get__(),如果有则执行它,否则只返回该属性。在 CPython 实现中,它首先检查 class.__dict__['x'],因此,我们将描述符放在类中。所以,真正的问题是,为什么要这样设计,而不是我所描述的那种我认为很自然的方式。
  • @hisen 那是因为这将禁止在实例中存储和成功检索描述符 - 在这种情况下,您将调用 __get__ 而不是返回描述符。
  • 谢谢,但是,为什么“禁止在实例中存储和成功检索描述符的可能性”?如果以这种方式实现:检查实例字典(如果定义,则评估 __get__(),否则其本身)-> 检查类字典(如果已定义,则评估 __get__(),否则本身)-> 检查基类字典(如果 __get__() 则评估定义,否则本身)。对我来说,这很容易理解,但是 Python 没有这样做,为什么?我认为一定有某种原因,我想知道它:-)
【解决方案2】:

让我们看一个例子:

class GetSetDesc(object):
    def __init__(self, value):
        self.value=value

    def __get__(self, obj, objtype):
        print("get_set_desc: Get")
        return self.value

    def __set__(self, obj, value):
        print("get_set_desc: Set")
        self.value=value

class SetDesc(object):
    def __init__(self, value):
        self.value=value

    def __set__(self, obj, value):
        print("set_desc: Set")
        self.value=value

class GetDesc(object):
    def __init__(self, value):
        self.value=value

    def __get__(self, obj, objtype):
        print("get_desc: Get")
        return self.value

class Test1(object):
    attr=10
    get_set_attr=10
    get_set_attr=GetSetDesc(5)
    set_attr=10
    set_attr=SetDesc(5)
    get_attr=10
    get_attr=GetDesc(5)

class Test2(object):
    def __init__(self):
        self.attr=10
        self.get_set_attr=10
        self.get_set_attr=GetSetDesc(5)
        self.set_attr=10
        self.set_attr=SetDesc(5)
        self.get_attr=10
        self.get_attr=GetDesc(5)

class Test3(Test1):
    def __init__(self):
        #changing values to see differce with superclass
        self.attr=100
        self.get_set_attr=100
        self.get_set_attr=GetSetDesc(50)
        self.set_attr=100
        self.set_attr=SetDesc(50)
        self.get_attr=100
        self.get_attr=GetDesc(50)

class Test4(Test1):
    pass


print("++Test 1 Start++")
t=Test1()

print("t.attr:", t.attr)
print("t.get_set_desc:", t.get_set_attr)
print("t.set_attr:", t.set_attr)
print("t.get_attr:", t.get_attr)

print("Class dict attr:", t.__class__.__dict__['attr'])
print("Class dict get_set_attr:", t.__class__.__dict__['get_set_attr'])
print("Class dict set_attr:", t.__class__.__dict__['set_attr'])
print("Class dict get_attr:", t.__class__.__dict__['get_attr'])

#These will obviously fail as instance dict is empty here
#print("Instance dict attr:", t.__dict__['attr'])
#print("Instance dict get_set_attr:", t.__dict__['get_set_attr'])
#print("Instance dict set_attr:", t.__dict__['set_attr'])
#print("Instance dict get_attr:", t.__dict__['get_attr'])

t.attr=20
t.get_set_attr=20
t.set_attr=20
t.get_attr=20

print("t.attr:", t.attr)
print("t.get_set_desc:", t.get_set_attr)
print("t.set_attr:", t.set_attr)
print("t.get_attr:", t.get_attr)

print("Class dict attr:", t.__class__.__dict__['attr'])
print("Class dict get_set_attr:", t.__class__.__dict__['get_set_attr'])
print("Class dict set_attr:", t.__class__.__dict__['set_attr'])
print("Class dict get_attr:", t.__class__.__dict__['get_attr'])

print("Instance dict attr:", t.__dict__['attr'])
#Next two will fail,
#because the descriptor for those variables has __set__
#on the class itself which was called with value 20,
#so the instance is not affected
#print("Instance dict get_set_attr:", t.__dict__['get_set_attr'])
#print("Instance dict set_attr:", t.__dict__['set_attr'])
print("Instance dict get_attr:", t.__dict__['get_attr'])

print("++Test 1 End++")


print("++Test 2 Start++")
t2=Test2()

print("t.attr:", t2.attr)
print("t.get_set_desc:", t2.get_set_attr)
print("t.set_attr:", t2.set_attr)
print("t.get_attr:", t2.get_attr)

#In this test the class is not affected, so these will fail
#print("Class dict attr:", t2.__class__.__dict__['attr'])
#print("Class dict get_set_attr:", t2.__class__.__dict__['get_set_attr'])
#print("Class dict set_attr:", t2.__class__.__dict__['set_attr'])
#print("Class dict get_attr:", t2.__class__.__dict__['get_attr'])

print("Instance dict attr:", t2.__dict__['attr'])
print("Instance dict get_set_attr:", t2.__dict__['get_set_attr'])
print("Instance dict set_attr:", t2.__dict__['set_attr'])
print("Instance dict get_attr:", t2.__dict__['get_attr'])

t2.attr=20
t2.get_set_attr=20
t2.set_attr=20
t2.get_attr=20

print("t.attr:", t2.attr)
print("t.get_set_desc:", t2.get_set_attr)
print("t.set_attr:", t2.set_attr)
print("t.get_attr:", t2.get_attr)

#In this test the class is not affected, so these will fail
#print("Class dict attr:", t2.__class__.__dict__['attr'])
#print("Class dict get_set_attr:", t2.__class__.__dict__['get_set_attr'])
#print("Class dict set_attr:", t2.__class__.__dict__['set_attr'])
#print("Class dict get_attr:", t2.__class__.__dict__['get_attr'])

print("Instance dict attr:", t2.__dict__['attr'])
print("Instance dict get_set_attr:", t2.__dict__['get_set_attr'])
print("Instance dict set_attr:", t2.__dict__['set_attr'])
print("Instance dict get_attr:", t2.__dict__['get_attr'])

print("++Test 2 End++")


print("++Test 3 Start++")
t3=Test3()

print("t.attr:", t3.attr)
print("t.get_set_desc:", t3.get_set_attr)
print("t.set_attr:", t3.set_attr)
print("t.get_attr:", t3.get_attr)

#These fail, because nothing is defined on Test3 class itself, but let's see its super below
#print("Class dict attr:", t3.__class__.__dict__['attr'])
#print("Class dict get_set_attr:", t3.__class__.__dict__['get_set_attr'])
#print("Class dict set_attr:", t3.__class__.__dict__['set_attr'])
#print("Class dict get_attr:", t3.__class__.__dict__['get_attr'])

#Checking superclass
print("Superclass dict attr:", t3.__class__.__bases__[0].__dict__['attr'])
print("Superclass dict get_set_attr:", t3.__class__.__bases__[0].__dict__['get_set_attr'])
print("Superclass dict set_attr:", t3.__class__.__bases__[0].__dict__['set_attr'])
print("Superclass dict get_attr:", t3.__class__.__bases__[0].__dict__['get_attr'])

print("Instance dict attr:", t3.__dict__['attr'])
#Next two with __set__ inside descriptor fail, because
#when the instance was created, the value inside the descriptor in superclass
#was redefined via __set__
#print("Instance dict get_set_attr:", t3.__dict__['get_set_attr'])
#print("Instance dict set_attr:", t3.__dict__['set_attr'])
print("Instance dict get_attr:", t3.__dict__['get_attr'])
#The one above does not fail, because it doesn't have __set__ in
#descriptor in superclass and therefore was redefined on instance

t3.attr=200
t3.get_set_attr=200
t3.set_attr=200
t3.get_attr=200

print("t.attr:", t3.attr)
print("t.get_set_desc:", t3.get_set_attr)
print("t.set_attr:", t3.set_attr)
print("t.get_attr:", t3.get_attr)

#print("Class dict attr:", t3.__class__.__dict__['attr'])
#print("Class dict get_set_attr:", t3.__class__.__dict__['get_set_attr'])
#print("Class dict set_attr:", t3.__class__.__dict__['set_attr'])
#print("Class dict get_attr:", t3.__class__.__dict__['get_attr'])

#Checking superclass
print("Superclass dict attr:", t3.__class__.__bases__[0].__dict__['attr'])
print("Superclass dict get_set_attr:", t3.__class__.__bases__[0].__dict__['get_set_attr'])
print("Superclass dict set_attr:", t3.__class__.__bases__[0].__dict__['set_attr'])
print("Superclass dict get_attr:", t3.__class__.__bases__[0].__dict__['get_attr'])

print("Instance dict attr:", t3.__dict__['attr'])
#Next two fail, they are in superclass, not in instance
#print("Instance dict get_set_attr:", t3.__dict__['get_set_attr'])
#print("Instance dict set_attr:", t3.__dict__['set_attr'])
print("Instance dict get_attr:", t3.__dict__['get_attr'])
#The one above succeds as it was redefined as stated in prior check

print("++Test 3 End++")


print("++Test 4 Start++")
t4=Test4()

print("t.attr:", t4.attr)
print("t.get_set_desc:", t4.get_set_attr)
print("t.set_attr:", t4.set_attr)
print("t.get_attr:", t4.get_attr)

#These again fail, as everything defined in superclass, not the class itself
#print("Class dict attr:", t4.__class__.__dict__['attr'])
#print("Class dict get_set_attr:", t4.__class__.__dict__['get_set_attr'])
#print("Class dict set_attr:", t4.__class__.__dict__['set_attr'])
#print("Class dict get_attr:", t4.__class__.__dict__['get_attr'])

#Checking superclass
print("Superclass dict attr:", t4.__class__.__bases__[0].__dict__['attr'])
print("Superclass dict get_set_attr:", t4.__class__.__bases__[0].__dict__['get_set_attr'])
print("Superclass dict set_attr:", t4.__class__.__bases__[0].__dict__['set_attr'])
print("Superclass dict get_attr:", t4.__class__.__bases__[0].__dict__['get_attr'])

#Again, everything is on superclass, not the instance
#print("Instance dict attr:", t4.__dict__['attr'])
#print("Instance dict get_set_attr:", t4.__dict__['get_set_attr'])
#print("Instance dict set_attr:", t4.__dict__['set_attr'])
#print("Instance dict get_attr:", t4.__dict__['get_attr'])

t4.attr=200
t4.get_set_attr=200
t4.set_attr=200
t4.get_attr=200

print("t.attr:", t4.attr)
print("t.get_set_desc:", t4.get_set_attr)
print("t.set_attr:", t4.set_attr)
print("t.get_attr:", t4.get_attr)

#Class is not affected by those assignments, next four fail
#print("Class dict attr:", t4.__class__.__dict__['attr'])
#print("Class dict get_set_attr:", t4.__class__.__dict__['get_set_attr'])
#print("Class dict set_attr:", t4.__class__.__dict__['set_attr'])
#print("Class dict get_attr:", t4.__class__.__dict__['get_attr'])

#Checking superclass
print("Superclass dict attr:", t4.__class__.__bases__[0].__dict__['attr'])
print("Superclass dict get_set_attr:", t4.__class__.__bases__[0].__dict__['get_set_attr'])
print("Superclass dict set_attr:", t4.__class__.__bases__[0].__dict__['set_attr'])
print("Superclass dict get_attr:", t4.__class__.__bases__[0].__dict__['get_attr'])

#Now, this one we redefined it succeeds
print("Instance dict attr:", t4.__dict__['attr'])
#This one fails it's still on superclass
#print("Instance dict get_set_attr:", t4.__dict__['get_set_attr'])
#Same here - fails, it's on superclass, because it has __set__
#print("Instance dict set_attr:", t4.__dict__['set_attr'])
#This one succeeds, no __set__ to call, so it was redefined on instance
print("Instance dict get_attr:", t4.__dict__['get_attr'])

print("++Test 4 End++")

输出:

++Test 1 Start++
t.attr: 10
get_set_desc: Get
t.get_set_desc: 5
t.set_attr: <__main__.SetDesc object at 0x02896ED0>
get_desc: Get
t.get_attr: 5
Class dict attr: 10
Class dict get_set_attr: <__main__.GetSetDesc object at 0x02896EB0>
Class dict set_attr: <__main__.SetDesc object at 0x02896ED0>
Class dict get_attr: <__main__.GetDesc object at 0x02896EF0>
get_set_desc: Set
set_desc: Set
t.attr: 20
get_set_desc: Get
t.get_set_desc: 20
t.set_attr: <__main__.SetDesc object at 0x02896ED0>
t.get_attr: 20
Class dict attr: 10
Class dict get_set_attr: <__main__.GetSetDesc object at 0x02896EB0>
Class dict set_attr: <__main__.SetDesc object at 0x02896ED0>
Class dict get_attr: <__main__.GetDesc object at 0x02896EF0>
Instance dict attr: 20
Instance dict get_attr: 20
++Test 1 End++
++Test 2 Start++
t.attr: 10
t.get_set_desc: <__main__.GetSetDesc object at 0x028A0350>
t.set_attr: <__main__.SetDesc object at 0x028A0370>
t.get_attr: <__main__.GetDesc object at 0x028A0330>
Instance dict attr: 10
Instance dict get_set_attr: <__main__.GetSetDesc object at 0x028A0350>
Instance dict set_attr: <__main__.SetDesc object at 0x028A0370>
Instance dict get_attr: <__main__.GetDesc object at 0x028A0330>
t.attr: 20
t.get_set_desc: 20
t.set_attr: 20
t.get_attr: 20
Instance dict attr: 20
Instance dict get_set_attr: 20
Instance dict set_attr: 20
Instance dict get_attr: 20
++Test 2 End++
++Test 3 Start++
get_set_desc: Set
get_set_desc: Set
set_desc: Set
set_desc: Set
t.attr: 100
get_set_desc: Get
t.get_set_desc: <__main__.GetSetDesc object at 0x02896FF0>
t.set_attr: <__main__.SetDesc object at 0x02896ED0>
t.get_attr: <__main__.GetDesc object at 0x028A03F0>
Superclass dict attr: 10
Superclass dict get_set_attr: <__main__.GetSetDesc object at 0x02896EB0>
Superclass dict set_attr: <__main__.SetDesc object at 0x02896ED0>
Superclass dict get_attr: <__main__.GetDesc object at 0x02896EF0>
Instance dict attr: 100
Instance dict get_attr: <__main__.GetDesc object at 0x028A03F0>
get_set_desc: Set
set_desc: Set
t.attr: 200
get_set_desc: Get
t.get_set_desc: 200
t.set_attr: <__main__.SetDesc object at 0x02896ED0>
t.get_attr: 200
Superclass dict attr: 10
Superclass dict get_set_attr: <__main__.GetSetDesc object at 0x02896EB0>
Superclass dict set_attr: <__main__.SetDesc object at 0x02896ED0>
Superclass dict get_attr: <__main__.GetDesc object at 0x02896EF0>
Instance dict attr: 200
Instance dict get_attr: 200
++Test 3 End++
++Test 4 Start++
t.attr: 10
get_set_desc: Get
t.get_set_desc: 200
t.set_attr: <__main__.SetDesc object at 0x02896ED0>
get_desc: Get
t.get_attr: 5
Superclass dict attr: 10
Superclass dict get_set_attr: <__main__.GetSetDesc object at 0x02896EB0>
Superclass dict set_attr: <__main__.SetDesc object at 0x02896ED0>
Superclass dict get_attr: <__main__.GetDesc object at 0x02896EF0>
get_set_desc: Set
set_desc: Set
t.attr: 200
get_set_desc: Get
t.get_set_desc: 200
t.set_attr: <__main__.SetDesc object at 0x02896ED0>
t.get_attr: 200
Superclass dict attr: 10
Superclass dict get_set_attr: <__main__.GetSetDesc object at 0x02896EB0>
Superclass dict set_attr: <__main__.SetDesc object at 0x02896ED0>
Superclass dict get_attr: <__main__.GetDesc object at 0x02896EF0>
Instance dict attr: 200
Instance dict get_attr: 200
++Test 4 End++

亲自尝试以了解描述符。但归根结底,我们在这里看到的是......

首先,刷新记忆的官方文档定义:

如果一个对象同时定义了__get__()__set__(),它被认为是一个 数据描述符。只定义__get__() 的描述符被称为 非数据描述符(它们通常用于方法,但其他 可以使用)。

从输出和失败的 sn-ps...

很明显,在重新分配引用描述符(任何类型)的名称之前,描述符会照常在 MRO 之后从类级别查找到超类,再到定义它的位置。 (参见测试 2,它在实例中定义并且不会被调用,而是使用简单值重新定义。)

现在重新分配名称后,事情就开始变得有趣了:

如果它是一个数据描述符(有__set__),那么真的不会发生任何魔法,分配给引用描述符的变量的值将传递给描述符的__set__,并在此方法中使用(关于上面的代码分配到self.value)。描述符首先在层次结构 ofc 中查找。顺便说一句,不带__get__ 的描述符本身返回,而不是与其__set__ 方法一起使用的值。

如果它是一个非数据描述符(只有__get__),那么它会被查找,但没有__set__ 方法,它会被“删除”,并且引用这个描述符的变量会被重新分配到尽可能低的级别(实例或子类,取决于我们在哪里定义它)。

因此描述符用于控制、修改分配给变量的数据,这些数据被制成描述符。所以这是有道理的,如果描述符是定义__set__ 的数据描述符,它可能想要解析您传递的数据,因此在实例字典键分配之前被调用。这就是为什么它首先放在层次结构中的原因。另一方面,如果它是一个只有__get__ 的非数据描述符,它可能不关心设置数据,甚至更多——它不能对数据集合做任何事情,所以它从分配链和数据被分配给实例字典键。

此外,新的样式类都是关于 MRO(方法 解析顺序),因此它会影响每个特征 - 描述符、属性(实际上也是描述符)、特殊方法等。描述符是基本上是在赋值或属性读取时调用的方法,因此可以像任何其他方法一样在类级别查找它们是有道理的。

如果您需要控制分配,但拒绝对变量进行任何更改,请使用数据描述符,但在其 __set__ 方法中引发和异常。

【讨论】:

  • 如果以这种方式实现(对于访问和赋值):检查实例字典(如果定义,则评估 __get/set__(),否则本身)-> 检查类字典(评估 __get/set__()如果已定义,则为自身)-> 检查基类字典(如果已定义,则评估 __get/set__(),否则为自身)。对我来说,这很容易理解,但是 Python 没有这样做,为什么?我认为一定有一些原因,我想知道它:-)
  • 首先,请参阅上文关于 MRO 的内容。其次,这是因为 OOP... 从概念上讲:行为通常在类级别定义,因为对于类的所有对象都是相同的,即所有汽车都在移动;另一方面,属性(这里不是指@property)属于某些类的特定对象-实例,即汽车A是红色的,汽车B是绿色的。如果您需要重新定义您的子类行为并在类级别进行,而不是为类的每个实例重新定义它。 (Ofc 也有例外,但一般的 OOP 概念同上)。进一步看下一条评论...
  • 现在,描述符重新定义了行为——它们管理/执行与对象属性相关的操作——这就是它们在类级别定义的原因。就这样。简短的回答 - 这完全是因为已建立的 OOP 概念。
  • 设计逻辑是,这个协议是用来描述行为的,而在OOP中,行为是针对类的。所以这个协议应该工作在类级别,数据描述符描述属性,所以属性查找优先链必须是这样的。正确的?这样想,一切都说得通。
  • 拥有描述符的理由写在上面,看起来你明白了。描述符具有优先权的理由来自它的角色,请参阅我的回答中底部段落的第三段。
【解决方案3】:

问题是过载之一。假设您有一个Descriptor 类,并且您将对象的一个​​属性设置为该类的一个实例:

class Descriptor:
    ...
    def __get__(self, parent, type=None):
       return 1

class MyObject:
    def __init__(self):
        self.foo = Descriptor()

mobj = MyObject()

在这种情况下,您有一个 非数据 描述符。由于 getter,任何访问 mobj.foo 的代码都会得到 1 的结果。

但是假设您尝试 store 到该属性?会发生什么?

答案:一个简单的条目将被添加到实例字典中,mobj.foo 将指向存储的任何值。

在这种情况下,如果您随后读取mobj.foo,您会返回哪个值? get 函数返回的“1”,还是字典中列出的最近存储的“实时”值?

对!在出现冲突的情况下,描述符会悄无声息地消失,您可以检索存储的任何内容。

【讨论】:

  • 在这种情况下,您应该在描述符中定义一个 'set' 以避免向实例字典插入新条目。
  • 实际上,如果代码访问mobj.foo,它会得到实际的描述符(如果你分配给mobj.foo,描述符也会被替换)。
  • @skyking 是的,我的错误,正如我在你的回答中回答的那样,我并不对如何使用描述符的方式感到困惑,但为什么查找链设计这种方式使得描述符应该始终放入类不是实例。我的问题可能很愚蠢,感谢您的耐心等待。
猜你喜欢
  • 2023-01-08
  • 1970-01-01
  • 2019-05-17
  • 2012-04-23
  • 1970-01-01
  • 2022-11-13
  • 1970-01-01
  • 1970-01-01
  • 2018-12-30
相关资源
最近更新 更多