Python 3.6文档中描述符协议的示例是否不正确?

时间:2017-10-06 16:20:18

标签: python python-3.6 descriptor

我是Python新手并查看其文档我遇到了以下示例协议示例,在我看来是不正确的。

看起来像是

class IntField:
    def __get__(self, instance, owner):
        return instance.__dict__[self.name]

    def __set__(self, instance, value):
        if not isinstance(value, int):
            raise ValueError(f'expecting integer in {self.name}')
        instance.__dict__[self.name] = value

    # this is the new initializer:
    def __set_name__(self, owner, name):
        self.name = name

class Model:
    int_field = IntField()

以下是我的考虑因素。

属性int_field是一个类范围的属性不是吗?

所以owner.__dict__会有这么一把钥匙。但是,在方法__get__中,使用了没有该密钥的instance.__dict__。因此,从使用该方法的角度来看,该方法不处理类范围的属性,而是处理实例范围的属性。

另一方面,方法__set__也不处理类范围的属性,但创建了一个实例范围的属性,因为使用了

instance.__dict__[self.name] = value

所以看起来这个类的每个实例都创建了自己的实例范围的属性。此外,对类的引用甚至不会传递给方法。

我是对的还是我错过了一些我还不知道的东西?

为了使我的考虑更清楚,逻辑上的示例等同于以下

class MyClass:
    int_field = 10

instance_of = MyClass();

instance_of.__dict__["int_field"] = 20

print( MyClass.int_field )
print( instance_of.int_field )

程序输出

10
20

除了名称之外,实例属性int_field与类范围属性int_field没有任何共同之处。

文档中的示例也是如此。直观地,可以预期它是附加到描述符的类范围属性。但事实并非如此。描述符只是借用了类范围属性的名称。另一方面,类范围的属性确实可以附加到描述符。

因此,我认为文档中的示例只会让读者感到困惑。

2 个答案:

答案 0 :(得分:3)

这个例子很好。

  

但是,在方法__get__中,使用了没有该密钥的instance.__dict__

实际设置instance.int_field后, 将成为这样一个密钥,它将调用属性设置器并分配密钥。

  

另一方面,方法__set__也不处理类范围的属性,但创建了一个实例范围的属性

setter不应该创建类属性。它为getter正在寻找的实例dict键指定。

答案 1 :(得分:2)

我想我可以看到你在这里错过了哪一个谜题:subl . -a 中的self描述符!这就是我们在__get__中设置状态的原因。通常在自身上设置状态(即在描述符上),因为instance.__dict__的每个实例都将共享该状态。

示例中没有任何矛盾,但此处使用的名称有些含糊不清。也许一些重命名会有所帮助:

Model

我还消除了类(class IntField: def __get__(self, obj, type_): # typically: self is an IntField(), obj is a Model(), type_ is Model return obj.__dict__[self.the_internal_name] def __set__(self, obj, value): if not isinstance(value, int): raise ValueError(f'expecting integer, but received {type(value)}') obj.__dict__[self.the_internal_name] = value def __set_name__(self, type_, name): # this is called at class definition time, i.e. descriptor init time self.the_internal_name = name + '_internal' class Model: int_field = IntField() )上的名称,描述符上的名称('int_field')以及在实例的字典中使用的名称( 'the_internal_name')。

上面代码中的所有'int_field_internal'都引用了描述符实例。还有一个描述符实例,它处理self的所有实例的属性访问。 Model(文档称为type_)将为ownerModel将是一个实例obj

示例中还缺少一段重要的代码。通常将一些逻辑放入Model(),以便允许访问描述符对象本身:

__get__

让我们尝试一下:

def __get__(self, obj, type_=None):
    print(f'Descriptor was accessed with obj {obj} and type_ {type_}')
    if obj is None:
        # the descriptor was invoked on the class instead of an instance
        # returning self here allows 'class attribute' access!
        return self  
    return obj.__dict__[self.the_internal_name]

因此,正如您所看到的,描述符实例(您可以将其视为类属性,如果需要)处理>>> m = Model() >>> m.__dict__ {} >>> m.int_field = 123 >>> m.__dict__ {'int_field_internal': 123} >>> m.int_field Descriptor was accessed with obj <__main__.Model object at 0x7fffe8186080> and type_ <class '__main__.Model'> 123 >>> Model.int_field Descriptor was accessed with obj None and type_ <class '__main__.Model'> <__main__.IntField at 0x7fffe8174748> >>> Model.int_field.__dict__ Descriptor was accessed with obj None and type_ <class '__main__.Model'> {'the_internal_name': 'int_field_internal'} 实例上的int_field名称以及{ {1}}本身。在实例上访问时,它会在实例上的Model名称中获取/设置状态。由于我已经消除了名称的歧义,您还可以访问Model作为正常属性获取/设置绕过描述符的相同值,如果您愿意(这是属性的常见做法) ,使用内部int_field_internal)。

在一般用法中,我们在实例dict中使用与描述符&#34;标签&#34;相同的名称。为此,因为......为什么不呢?此阴影对实例名称的正常属性访问,因为描述符协议具有优先级。但你仍然可以绕过描述符,如下所示:

m.int_field_internal

看看你是否可以猜出_name将返回什么,为什么,并尝试一下!然后尝试创建一个这样的新模型:

print(m.int_field)  # this access goes through the descriptor object
print(m.__dict__['int_field'])  # this is 'raw' access

然后你会看到如何使用Model.int_field.the_internal_name。描述符允许您在常规属性访问期间(无论是获取,设置还是删除...)进行任何您想要的操作。您可以根据需要使用它来管理类,实例或描述符本身的状态 - 这就是为什么它可能令人困惑的解释,但这也是它的强大和灵活性的地方功能来自。