我试图了解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'} 我希望事情现在都清楚了:)

其他回答

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

容易消化(有例子)解释类中的__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的属性类型是如何实现的。描述符简单地实现__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类中),但这表明可以做什么…

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