我试图了解Python的描述符是什么以及它们有什么用处。我明白它们是如何运作的,但我有一些疑问。考虑下面的代码:

class Celsius(object):
    def __init__(self, value=0.0):
        self.value = float(value)
    def __get__(self, instance, owner):
        return self.value
    def __set__(self, instance, value):
        self.value = float(value)


class Temperature(object):
    celsius = Celsius()

为什么我需要描述符类? 这里的实例和所有者是什么?(在__get__)。这些参数的目的是什么? 我如何调用/使用这个例子?


当前回答

为什么我需要描述符类?

它为您提供了对属性如何工作的额外控制。例如,如果你习惯了Java中的getter和setter,那么Python就是这样做的。一个优点是它看起来就像一个属性(语法上没有变化)。因此,您可以从一个普通属性开始,然后,当您需要做一些奇特的事情时,切换到一个描述符。

属性只是一个可变值。描述符允许您在读取或设置(或删除)值时执行任意代码。因此,您可以想象使用它来将一个属性映射到数据库中的一个字段,例如—一种ORM。

另一种用法可能是通过在__set__中抛出异常来拒绝接受新值——有效地使“属性”为只读。

这里的实例和所有者是什么?(在__get__)。这些参数的目的是什么?

这是非常微妙的(我在这里写一个新答案的原因是——我在想同样的事情时发现了这个问题,并没有发现现有的答案那么好)。

描述符定义在类上,但通常从实例调用。当从实例中调用它时,实例和所有者都被设置了(你可以从实例中计算出所有者,所以看起来有点毫无意义)。但是当从类中调用时,只设置了owner -这就是为什么它在那里。

这只需要__get__,因为它是唯一一个可以在类上调用的。如果你设置了类值,你就设置了描述符本身。删除也是如此。这就是为什么这里不需要所有者。

我如何调用/使用这个例子?

这里有一个使用类似类的很酷的技巧:

class Celsius:

    def __get__(self, instance, owner):
        return 5 * (instance.fahrenheit - 32) / 9

    def __set__(self, instance, value):
        instance.fahrenheit = 32 + 9 * value / 5


class Temperature:

    celsius = Celsius()

    def __init__(self, initial_f):
        self.fahrenheit = initial_f


t = Temperature(212)
print(t.celsius)
t.celsius = 0
print(t.fahrenheit)

(我使用的是Python 3;对于python 2,你需要确保这些分区是/ 5.0和/ 9.0)。出:

100.0
32.0

现在,在python中还有其他更好的方法来实现同样的效果(例如,如果celsius是一个属性,这是相同的基本机制,但将所有源放在Temperature类中),但这表明可以做什么…

其他回答

描述符是Python的属性类型是如何实现的。描述符简单地实现__get__, __set__等,然后在它的定义中添加到另一个类(就像上面对Temperature类所做的那样)。例如:

temp=Temperature()
temp.celsius #calls celsius.__get__

访问您分配给描述符的属性(在上面的例子中是celsius)将调用适当的描述符方法。

__get__中的instance是类的实例(因此在上面,__get__将接收temp,而owner是带有描述符的类(因此它将是Temperature)。

您需要使用一个描述符类来封装为其提供支持的逻辑。这样,如果描述符用于缓存一些昂贵的操作(例如),它可以将值存储在自己而不是它的类上。

一篇关于描述符的文章可以在这里找到。

编辑:正如jchl在评论中指出的,如果您只是尝试Temperature。摄氏度,实例将为None。

为什么我需要描述符类?

它为您提供了对属性如何工作的额外控制。例如,如果你习惯了Java中的getter和setter,那么Python就是这样做的。一个优点是它看起来就像一个属性(语法上没有变化)。因此,您可以从一个普通属性开始,然后,当您需要做一些奇特的事情时,切换到一个描述符。

属性只是一个可变值。描述符允许您在读取或设置(或删除)值时执行任意代码。因此,您可以想象使用它来将一个属性映射到数据库中的一个字段,例如—一种ORM。

另一种用法可能是通过在__set__中抛出异常来拒绝接受新值——有效地使“属性”为只读。

这里的实例和所有者是什么?(在__get__)。这些参数的目的是什么?

这是非常微妙的(我在这里写一个新答案的原因是——我在想同样的事情时发现了这个问题,并没有发现现有的答案那么好)。

描述符定义在类上,但通常从实例调用。当从实例中调用它时,实例和所有者都被设置了(你可以从实例中计算出所有者,所以看起来有点毫无意义)。但是当从类中调用时,只设置了owner -这就是为什么它在那里。

这只需要__get__,因为它是唯一一个可以在类上调用的。如果你设置了类值,你就设置了描述符本身。删除也是如此。这就是为什么这里不需要所有者。

我如何调用/使用这个例子?

这里有一个使用类似类的很酷的技巧:

class Celsius:

    def __get__(self, instance, owner):
        return 5 * (instance.fahrenheit - 32) / 9

    def __set__(self, instance, value):
        instance.fahrenheit = 32 + 9 * value / 5


class Temperature:

    celsius = Celsius()

    def __init__(self, initial_f):
        self.fahrenheit = initial_f


t = Temperature(212)
print(t.celsius)
t.celsius = 0
print(t.fahrenheit)

(我使用的是Python 3;对于python 2,你需要确保这些分区是/ 5.0和/ 9.0)。出:

100.0
32.0

现在,在python中还有其他更好的方法来实现同样的效果(例如,如果celsius是一个属性,这是相同的基本机制,但将所有源放在Temperature类中),但这表明可以做什么…

在详细介绍描述符之前,了解Python中的属性查找是如何工作的可能很重要。这假设类没有元类,并且它使用__getattribute__的默认实现(两者都可以用于“自定义”行为)。

属性查找的最佳说明(在Python 3中)。x或Python 2.x中的新风格类)在这种情况下来自理解Python元类(ionel的代码日志)。该图像使用:来代替“不可自定义属性查找”。

这表示在Class的实例上查找一个属性foobar:

这里有两个重要的条件:

如果实例类有一个属性名条目,并且它有__get__和__set__。 如果实例中没有对应属性名的条目,但类中有,并且类中有__get__。

这就是描述符的作用:

同时具有__get__和__set__的数据描述符。 只有__get__.的非数据描述符。

在这两种情况下,返回值通过__get__调用,实例作为第一个参数,类作为第二个参数。

类属性查找的查找甚至更加复杂(参见类属性查找的示例(在上面提到的博客中))。

让我们来谈谈你的具体问题:

为什么我需要描述符类?

在大多数情况下,您不需要编写描述符类!然而,你可能是一个非常普通的终端用户。例如函数。函数是描述符,这就是函数如何作为方法使用,并将自隐式作为第一个参数传递。

def test_function(self):
    return self

class TestClass(object):
    def test_method(self):
        ...

如果你在一个实例上查找test_method,你会得到一个“bound method”:

>>> instance = TestClass()
>>> instance.test_method
<bound method TestClass.test_method of <__main__.TestClass object at ...>>

类似地,你也可以通过手动调用__get__方法来绑定一个函数(不推荐,只是为了说明目的):

>>> test_function.__get__(instance, TestClass)
<bound method test_function of <__main__.TestClass object at ...>>

你甚至可以称之为“自我约束方法”:

>>> test_function.__get__(instance, TestClass)()
<__main__.TestClass at ...>

请注意,我没有提供任何参数,该函数确实返回了我绑定的实例!

函数是非数据描述符!

数据描述符的一些内置示例是属性。忽略getter、setter和delete属性描述符是(摘自描述符HowTo指南“属性”):

class Property(object):
    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        if doc is None and fget is not None:
            doc = fget.__doc__
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("unreadable attribute")
        return self.fget(obj)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(obj, value)

    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        self.fdel(obj)

因为它是一个数据描述符,当你查找属性的“name”时,它就会被调用,它只是委托给带有@property, @name装饰的函数。Setter和@name.deleter(如果存在)。

标准库中还有其他几种描述符,例如staticmethod、classmethod。

描述符的意义很简单(尽管您很少需要它们):抽象用于属性访问的公共代码。Property是对实例变量访问的抽象,function是对方法的抽象,staticmethod是对不需要实例访问的方法的抽象,classmethod是对需要类访问而不是实例访问的方法的抽象(这有点简化)。

另一个例子是类属性。

一个有趣的例子(使用Python 3.6中的__set_name__)也可以是只允许特定类型的属性:

class TypedProperty(object):
    __slots__ = ('_name', '_type')
    def __init__(self, typ):
        self._type = typ

    def __get__(self, instance, klass=None):
        if instance is None:
            return self
        return instance.__dict__[self._name]

    def __set__(self, instance, value):
        if not isinstance(value, self._type):
            raise TypeError(f"Expected class {self._type}, got {type(value)}")
        instance.__dict__[self._name] = value

    def __delete__(self, instance):
        del instance.__dict__[self._name]

    def __set_name__(self, klass, name):
        self._name = name

然后你可以在类中使用描述符:

class Test(object):
    int_prop = TypedProperty(int)

和它玩了一会儿:

>>> t = Test()
>>> t.int_prop = 10
>>> t.int_prop
10

>>> t.int_prop = 20.0
TypeError: Expected class <class 'int'>, got <class 'float'>

或者一个“惰性属性”:

class LazyProperty(object):
    __slots__ = ('_fget', '_name')
    def __init__(self, fget):
        self._fget = fget

    def __get__(self, instance, klass=None):
        if instance is None:
            return self
        try:
            return instance.__dict__[self._name]
        except KeyError:
            value = self._fget(instance)
            instance.__dict__[self._name] = value
            return value

    def __set_name__(self, klass, name):
        self._name = name

class Test(object):
    @LazyProperty
    def lazy(self):
        print('calculating')
        return 10

>>> t = Test()
>>> t.lazy
calculating
10
>>> t.lazy
10

在这些情况下,将逻辑移到公共描述符中可能是有意义的,但是也可以用其他方法解决这些问题(但可能会重复一些代码)。

这里的实例和所有者是什么?(在__get__)。这些参数的目的是什么?

这取决于您如何查找属性。如果你在一个实例上查找属性,那么:

第二个参数是用于查找属性的实例 第三个参数是实例的类

如果你在类上查找属性(假设描述符是在类上定义的):

第二个参数是None 第三个参数是用于查找属性的类

因此,如果您想在进行类级查找时自定义行为(因为实例为None),那么基本上第三个参数是必要的。

我如何调用/使用这个例子?

你的例子基本上是一个属性,它只允许可以转换为float的值,并且在类的所有实例之间共享(并且在类上-尽管只能在类上使用“read”访问,否则你将替换描述符实例):

>>> t1 = Temperature()
>>> t2 = Temperature()

>>> t1.celsius = 20   # setting it on one instance
>>> t2.celsius        # looking it up on another instance
20.0

>>> Temperature.celsius  # looking it up on the class
20.0

这就是为什么描述符通常使用第二个参数(instance)来存储值,以避免共享它。然而,在某些情况下,可能需要在实例之间共享值(尽管目前我想不出具体的场景)。然而,对于温度类的celsius属性来说,这几乎没有任何意义。除了纯粹的学术练习。

你会看到https://docs.python.org/3/howto/descriptor.html#properties

class Property(object):
    "Emulate PyProperty_Type() in Objects/descrobject.c"

    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        if doc is None and fget is not None:
            doc = fget.__doc__
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("unreadable attribute")
        return self.fget(obj)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(obj, value)

    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        self.fdel(obj)

    def getter(self, fget):
        return type(self)(fget, self.fset, self.fdel, self.__doc__)

    def setter(self, fset):
        return type(self)(self.fget, fset, self.fdel, self.__doc__)

    def deleter(self, fdel):
        return type(self)(self.fget, self.fset, fdel, self.__doc__)

我尝试了Andrew Cooke回答的代码(根据建议做了一些小修改)。(我正在运行python 2.7)。

代码:

#!/usr/bin/env python
class Celsius:
    def __get__(self, instance, owner): return 9 * (instance.fahrenheit + 32) / 5.0
    def __set__(self, instance, value): instance.fahrenheit = 32 + 5 * value / 9.0

class Temperature:
    def __init__(self, initial_f): self.fahrenheit = initial_f
    celsius = Celsius()

if __name__ == "__main__":

    t = Temperature(212)
    print(t.celsius)
    t.celsius = 0
    print(t.fahrenheit)

结果:

C:\Users\gkuhn\Desktop>python test2.py
<__main__.Celsius instance at 0x02E95A80>
212

使用Python在3之前,确保你的子类from object将使描述符正确工作,因为get魔法不适用于旧风格的类。