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


当前回答

容易消化(有例子)解释类中的__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'} 我希望事情现在都清楚了:)

其他回答

为什么我需要描述符类?

它为您提供了对属性如何工作的额外控制。例如,如果你习惯了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类中),但这表明可以做什么…

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

容易消化(有例子)解释类中的__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'} 我希望事情现在都清楚了:)

我试图理解Python的描述符是什么以及它们可以用于什么。

描述符是类名称空间中的对象,用于管理实例属性(如插槽、属性或方法)。例如:

class HasDescriptors:
    __slots__ = 'a_slot' # creates a descriptor
    
    def a_method(self):  # creates a descriptor
        "a regular method"
    
    @staticmethod        # creates a descriptor
    def a_static_method():
        "a static method"
    
    @classmethod         # creates a descriptor
    def a_class_method(cls):
        "a class method"
    
    @property            # creates a descriptor
    def a_property(self):
        "a property"

# even a regular function:
def a_function(some_obj_or_self):      # creates a descriptor
    "create a function suitable for monkey patching"

HasDescriptors.a_function = a_function     # (but we usually don't do this)

从学理上讲,描述符是具有以下任何特殊方法的对象,这些方法可以称为“描述符方法”:

__get__:非数据描述符方法,例如在方法/函数上 __set__:数据描述符方法,例如在属性实例或插槽上 __delete__:数据描述符方法,同样由属性或插槽使用

这些描述符对象是其他对象类名称空间中的属性。也就是说,它们存在于类对象的__dict__中。

描述符对象以编程方式管理普通表达式、赋值或删除中的点查找(例如foo.descriptor)的结果。

函数/方法、绑定方法、属性、类方法和staticmethod都使用这些特殊的方法来控制如何通过点查找访问它们。

数据描述符(如属性)允许基于更简单的对象状态对属性进行延迟计算,从而允许实例使用比预先计算每个可能属性更少的内存。

另一个数据描述符是由__slots__创建的member_descriptor,通过让类将数据存储在可变的元组类数据结构中,而不是更灵活但占用空间的__dict__,可以节省内存(和更快的查找)。

非数据描述符,实例和类方法,从它们的非数据描述符方法__get__中获得它们的隐式第一个参数(通常分别命名为self和cls)——这就是静态方法如何知道不要有隐式第一个参数。

大多数Python用户只需要学习描述符的高级用法,而不需要进一步学习或理解描述符的实现。

但是了解描述符的工作原理可以让人对掌握Python更有信心。

什么是描述符?

描述符是具有以下任何方法(__get__, __set__或__delete__)的对象,旨在通过点查找来使用,就像它是实例的典型属性一样。对于带有描述符对象的所有者对象obj_instance:

obj_instance.descriptor调用 描述符。__get__(self, obj_instance, owner_class)返回一个值 这就是属性上的所有方法和get的工作方式。 Obj_instance.descriptor = value调用 描述符。__set__(self, obj_instance, value)返回None 这就是属性的setter的工作方式。 Del obj_instance.descriptor调用 描述符。__delete__(self, obj_instance)返回None 这就是属性上的删除器的工作方式。

Obj_instance是其类包含描述符对象的实例的实例。Self是描述符的实例(对于obj_instance类可能只有一个)

要用代码定义,如果一个对象的属性集与任何必需的属性相交,那么它就是一个描述符:

def has_descriptor_attrs(obj):
    return set(['__get__', '__set__', '__delete__']).intersection(dir(obj))

def is_descriptor(obj):
    """obj can be instance of descriptor or the descriptor class"""
    return bool(has_descriptor_attrs(obj))

数据描述符有__set__和/或__delete__。 非数据描述符既没有__set__也没有__delete__。

def has_data_descriptor_attrs(obj):
    return set(['__set__', '__delete__']) & set(dir(obj))

def is_data_descriptor(obj):
    return bool(has_data_descriptor_attrs(obj))

内置描述符对象示例:

classmethod staticmethod 财产 一般函数

数据描述符

我们可以看到classmethod和staticmethod是非数据描述符:

>>> is_descriptor(classmethod), is_data_descriptor(classmethod)
(True, False)
>>> is_descriptor(staticmethod), is_data_descriptor(staticmethod)
(True, False)

两者都只有__get__方法:

>>> has_descriptor_attrs(classmethod), has_descriptor_attrs(staticmethod)
(set(['__get__']), set(['__get__']))

注意,所有函数都是非数据描述符:

>>> def foo(): pass
... 
>>> is_descriptor(foo), is_data_descriptor(foo)
(True, False)

数据描述符,属性

然而,属性是一个数据描述符:

>>> is_data_descriptor(property)
True
>>> has_descriptor_attrs(property)
set(['__set__', '__get__', '__delete__'])

点查找顺序

这些是重要的区别,因为它们会影响点查找的查找顺序。

obj_instance.attribute

首先,上面的代码查看属性是否是实例类上的Data-Descriptor, 如果不是,它会查看该属性是否在obj_instance的__dict__中,然后 它最终会退回到非数据描述符。

这种查找顺序的结果是,像函数/方法这样的非数据描述符可以被实例覆盖。

概述和下一步

我们已经了解到,描述符是具有__get__、__set__或__delete__中的任意一个对象。这些描述符对象可以用作其他对象类定义的属性。现在,我们将以您的代码为例,看看它们是如何使用的。


从问题分析代码

下面是你的代码,后面是你的问题和答案:

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()

为什么我需要描述符类?

你的描述符确保你总是有一个float的class属性Temperature,并且你不能使用del删除这个属性:

>>> t1 = Temperature()
>>> del t1.celsius
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: __delete__

否则,您的描述符将忽略所有者类和所有者的实例,而是将状态存储在描述符中。你可以通过一个简单的class属性轻松地在所有实例之间共享状态(只要你总是将它设置为类的float,并且永远不会删除它,或者你的代码的用户愿意这样做):

class Temperature(object):
    celsius = 0.0

这将使您的行为与您的示例完全相同(参见下面对问题3的响应),但使用了python的内置(属性),并且将被认为更习惯:

class Temperature(object):
    _celsius = 0.0
    @property
    def celsius(self):
        return type(self)._celsius
    @celsius.setter
    def celsius(self, value):
        type(self)._celsius = float(value)

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

Instance是调用描述符的所有者的实例。所有者是一个类,其中描述符对象用于管理对数据点的访问。关于更多描述性变量名,请参阅本答案第一段旁边定义描述符的特殊方法的描述。

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

下面是一个演示:

>>> t1 = Temperature()
>>> t1.celsius
0.0
>>> t1.celsius = 1
>>> 
>>> t1.celsius
1.0
>>> t2 = Temperature()
>>> t2.celsius
1.0

你不能删除属性:

>>> del t2.celsius
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: __delete__

你不能给一个不能被转换成浮点数的变量赋值:

>>> t1.celsius = '0x02'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 7, in __set__
ValueError: invalid literal for float(): 0x02

否则,您在这里拥有的是所有实例的全局状态,它通过分配给任何实例来管理。

大多数有经验的Python程序员实现这一结果的预期方式是使用属性装饰器,它在底层使用相同的描述符,但将行为带入所有者类的实现中(同样,如上所定义):

class Temperature(object):
    _celsius = 0.0
    @property
    def celsius(self):
        return type(self)._celsius
    @celsius.setter
    def celsius(self, value):
        type(self)._celsius = float(value)

它具有与原始代码完全相同的预期行为:

>>> t1 = Temperature()
>>> t2 = Temperature()
>>> t1.celsius
0.0
>>> t1.celsius = 1.0
>>> t2.celsius
1.0
>>> del t1.celsius
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't delete attribute
>>> t1.celsius = '0x02'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 8, in celsius
ValueError: invalid literal for float(): 0x02

结论

我们已经讨论了定义描述符的属性、数据描述符和非数据描述符之间的区别、使用它们的内置对象以及关于使用的特定问题。

那么,你会怎么用这个问题的例子呢?我希望你不会。我希望您从我的第一个建议(一个简单的类属性)开始,如果您认为有必要,可以转向第二个建议(属性装饰器)。

在详细介绍描述符之前,了解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属性来说,这几乎没有任何意义。除了纯粹的学术练习。