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


当前回答

你会看到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__)

其他回答

我试图理解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

结论

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

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

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

为什么我需要描述符类?

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

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

你会看到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__)