什么是实现描述符的正确方法?

时间:2017-09-27 10:40:33

标签: python descriptor

考虑此代码:在python 3.6上运行

Bar将值分配给描述符实例

Bat将值分配给包含的类实例。

我见过的代码示例(并且习惯了我无尽的沮丧)使用了Bar示例。例如site

并从python docs

从使用Bar示例的输出中可以看出,类的两个实例不能使用相同的描述符。

或者我错过了什么?

class DescriptorA(object):
    value = None
    def __get__(self, instance, owner):
        return self.value

    def __set__(self, instance, value):
        self.value = value

class DescriptorB(object):
    _value = None
    def __get__(self, instance, owner):
        return instance._value

    def __set__(self, instance, value):
        instance._value = value


class Bar(object):
    foo = DescriptorA()
    def __init__(self, foo):
        self.foo = foo

class Bat(object):
    foo = DescriptorB()
    def __init__(self, foo):
        self.foo = foo


print('BAR')
a = Bar(1)
print('a', a.foo)

b = Bar(2)
print('b', b.foo)
print('Checking a')
print('a', a.foo)

print('BAT')
c = Bat(3)
print('c', c.foo)

d = Bat(4)
print('d', d.foo)
print('Checking c')
print('c', c.foo)

输出

BAR
a 1
b 2
Checking a
a 2
BAT
c 3
d 4
Checking c
c 3

更新

只想添加此内容。回应好的答案。不使用描述符时,仍然使用类属性。我们得到不同的行为。这就是为什么我犯了使用DescriptorA Eg的错误。

class Bar(object):
    foo = None
    def __init__(self, foo):
        self.foo = foo

class Bat(object):
    foo = None
    def __init__(self, foo):
        self.foo = foo


print('BAR')
a = Bar(1)
print('a', a.foo)

b = Bar(2)
print('b', b.foo)
print('Checking a')
print('a', a.foo)

print('BAT')
c = Bat(3)
print('c', c.foo)

d = Bat(4)
print('d', d.foo)
print('Checking c')
print('c', c.foo)
BAR
a 1
b 2
Checking a
a 1
BAT
c 3
d 4
Checking c
c 3

3 个答案:

答案 0 :(得分:1)

描述符是在类级别定义的,并且该类中只有该描述符的一个实例。因此,在第一个描述符即DescriptorA中,您将值存储为描述符而不是instance对象上的变量。显然,当您实例化另一个实例时,该值将被覆盖。

存储在描述符中的任何值对于分配了描述符的类的所有实例都将保持不变。这就是为什么DescriptorB有效并且是使用描述符而不是第一个的正确方法,除非你的用例需要在实例之间保持相同的变量。

答案 1 :(得分:1)

描述符是类属性(它们必须是描述符协议才能工作)。作为类属性意味着所有类的实例(以及它的子类)共享一个单独的描述符实例,因此您在类BarDescriptorA中观察到的是预期的行为。

并不意味着"一个类的两个实例不能使用相同的描述符(实例)" - 他们确实这样做了,这就是为什么你有这种行为 - 但是你不能在描述符实例上存储每个实例的值,至少不是那么简单。

一种可能的解决方案是在描述符中维护id(instance):instance_value映射,即:

class DescriptorA(object):
    def __init__(self, default=None):
        self._values = {}
        self._default = default
    def __get__(self, instance, cls):
        if instance is None:
            return self
        return self._values.get(id(instance), self._default)
    def __set__(self, instance, value):
        self._values[id(instance)] = value

但这有一些缺点,第一个明显的缺点是当一个实例被垃圾收集时你的_values dict不会被清除。在漫长的过程中,它最终会吃掉一些公羊...

编辑:您的更新中的代码使用类属性。拥有eponym类属性与此无关 - 初始化程序设置了一个value 实例属性,该属性隐藏了类级别(实际上从未在您的代码段中使用过)。

如果你想要一个有关类属性的有意义的例子,使用一个可变对象并改变它而不是创建一个实例属性:

>>> class Foo(object):
...    bar = []
...    def __init__(self, baaz):
...        self.baaz = baaz
...        self.bar.append(baaz)
... 
>>> f1 = Foo("foo1")
>>> f1.baaz
'foo1'
>>> f1.bar
['foo1']
>>> f2 = Foo("foo2")
>>> f1.baaz
'foo1'
>>> f2.baaz
'foo2'
>>> f1.bar
['foo1', 'foo2']
>>> f2.bar
['foo1', 'foo2']
>>> 

答案 2 :(得分:1)

这实际上取决于您的用例如何存储变量。

我们有4个对象,每个对象都有自己的一组变量:

  • 描述符类
  • 描述符的实例
  • 正常班级
  • 普通班级的实例

通常描述符实例存储在"普通类"因为在实例中存储描述符时不会调用描述符协议。你也可以"去meta"并在描述符中使用元类或描述符上的描述符,但是为了保持简洁和保持理智而忽略这些(它不是很难,但可能有点过于宽泛)。

所以,如果您的DescriptorA存储:

  • value = None在描述符类
  • 中 描述符实例中的
  • value = ?(至少在调用__set__至少一次
  • 之后) 正常班级中的
  • foo = descriptor instance
  • 类实例中没有任何内容

如果您存储DescriptorB

  • _value = None在描述符类
  • 描述符实例中没有任何内容
  • 正常班级中的
  • foo = descriptor instance
  • 正常班级中的
  • _value = ?

看到区别?在第一种情况下,普通类的不同实例访问相同的描述符实例,因此所有内容都是共享的。在第二种情况下,您将所有内容存储在类的实例中,并且不在描述符实例中存储任何内容,因此不会共享任何内容。

请注意,您的DescriptorB似乎很奇怪,为什么在您从未使用它时将_value = None存储在描述符类中?请记住,您访问了普通类实例的_value而不是_value中描述符实例的__get__

正如我之前所说,这取决于你的用例选择哪种方法。通常,您希望拥有一些共享属性和一些每实例属性。但您也可以在描述符的所有实例之间共享属性,并且您还可以在__get__中访问普通类实例的类型,并在type(instance)中使用__set__您也可以修改类正常班级的属性。

例如Python文档中的示例:

class RevealAccess(object):
    """A data descriptor that sets and returns values
       normally and prints a message logging their access.
    """

    def __init__(self, initval=None, name='var'):
        self.val = initval
        self.name = name

    def __get__(self, obj, objtype):
        print('Retrieving', self.name)
        return self.val

    def __set__(self, obj, val):
        print('Updating', self.name)
        self.val = val

>>> class MyClass(object):
...     x = RevealAccess(10, 'var "x"')
...     y = 5
...

他们故意为类变量创建了一个描述符。在那种情况下,没有"实例"它是否共享无关紧要,因为默认情况下实例会共享一个类变量。这意味着即使您在实例上设置变量,它也会针对所有其他实例进行更改。

因此,如果您不希望共享描述符实例变量,则不应该使用它。但是,您应该将它们用于应该共享的所有内容(例如属性的名称等)。

也许有趣的可能是"方式" Python查找属性。我通常会发现this blog的这张图片非常有用:

enter image description here