我试图了解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__)。这些参数的目的是什么? 我如何调用/使用这个例子?


当前回答

我尝试了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魔法不适用于旧风格的类。

其他回答

描述符是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类中),但这表明可以做什么…

为什么我需要描述符类?

灵感来自Buciano Ramalho的Fluent Python

想象你有这样一门课

class LineItem:
     price = 10.9
     weight = 2.1
     def __init__(self, name, price, weight):
          self.name = name
          self.price = price
          self.weight = weight

item = LineItem("apple", 2.9, 2.1)
item.price = -0.9  # it's price is negative, you need to refund to your customer even you delivered the apple :(
item.weight = -0.8 # negative weight, it doesn't make sense

我们应该验证重量和价格,避免给它们分配一个负数,如果我们使用描述符作为代理,我们可以写更少的代码

class Quantity(object):
    __index = 0

    def __init__(self):
        self.__index = self.__class__.__index
        self._storage_name = "quantity#{}".format(self.__index)
        self.__class__.__index += 1

    def __set__(self, instance, value):
        if value > 0:
            setattr(instance, self._storage_name, value)
        else:
           raise ValueError('value should >0')

   def __get__(self, instance, owner):
        return getattr(instance, self._storage_name)

然后像这样定义类LineItem:

class LineItem(object):
     weight = Quantity()
     price = Quantity()

     def __init__(self, name, weight, price):
         self.name = name
         self.weight = weight
         self.price = price

我们可以扩展Quantity类来进行更常见的验证

容易消化(有例子)解释类中的__get__ & __set__ & __call__,什么是所有者,实例?

在投入工作之前,要记住以下几点:

__get__ __set__ are called descriptors of the class to work/save their internal attributes namely: __name__ (name of class/owner class), variables - __dict__ etc. I will explain what is an owner later Descriptors are used in design patterers more commonly, for example, with decorators (to abstract things out). You can consider it's more often used in software architecture design to make things less redundant and more readable (seems ironical). Thus abiding SOLID and DRY principles. If you are not designing software that should abide by SOLID and DRY principles, you probably don't need them, but it's always wise to understand them.

1. 考虑下面的代码:

class Method:
    def __init__(self, name):
        self.name = name
    def __call__(self, instance, arg1, arg2):
        print(f"{self.name}: {instance} called with {arg1} and {arg2}")


class MyClass:
    method = Method("Internal call")

instance = MyClass()


instance.method("first", "second")

# Prints:TypeError: __call__() missing 1 required positional argument: 'arg2'

当实例。方法("first", "second")被调用,__call__方法从method类被调用(调用方法使类对象像函数一样可调用-每当类实例被调用__call__时被初始化),并分配以下参数:instance: "first", arg1: "second",最后一个arg2被忽略,这将打印错误:

2. 如何解决?

由于__call__将instance作为第一个参数(instance, arg1, arg2),但是什么实例? 实例是调用描述符类(Method)的主类(MyClass)的实例。instance = MyClass()是实例那么谁是所有者呢?然而,在我们的描述符类(method)中没有方法将它识别为实例。这就是我们需要__get__方法的地方。再次考虑下面的代码:



from types import MethodType
class Method:
    def __init__(self, name):
        self.name = name
    def __call__(self, instance, arg1, arg2):
        print(f"{self.name}: {instance} called with {arg1} and {arg2}")
    def __set__(self, instance, value):
        self.value = value
        instance.__dict__["method"] = value
    def __get__(self, instance, owner):
        if instance is None:
            return self
        print (instance, owner)
        return MethodType(self, instance)   


class MyClass:
    method = Method("Internal call")

instance = MyClass()


instance.method("first", "second") 
# Prints: Internal call: <__main__.MyClass object at 0x7fb7dd989690> called with first and second

根据文档,先忘掉set吧:

__get__“调用来获取所有者类的属性(类属性访问)或该类的实例的属性(实例属性访问)。”

如果你这样做:

打印:< __main__。MyClass对象0x7fb7dd9eab90> <class '__main__。MyClass的>

这意味着instance: MyClass的对象,即instance Owner是MyClass本身

3.__set__解释:

__set__用于设置类__dict__对象中的某个值(假设使用命令行)。用于设置set的内部值的命令是:instance.descriptor = 'value' #,在这种情况下,descriptor是method

(实例。__dict__["method"] = value在代码中只是更新描述符的__dict__对象) 所以请执行:instance。方法= 'value'现在要检查在__set__方法中是否设置了value = 'value',我们可以访问descriptor方法的__dict__对象。 做的事: instance.method。__dict__打印:{“_name”:“内部电话”,“价值”:“价值”} 或者你可以使用vars(instance.method)检查__dict__值 打印:{'name': '内部调用','value': 'value'} 我希望事情现在都清楚了:)

我尝试了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魔法不适用于旧风格的类。