理解__get__和__set__以及Python描述符

我是尝试来理解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()
  1. 为什么我需要描述符类?

  2. 这里的instanceowner是什么?(在__get__)。这些参数的目的是什么?

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

176134 次浏览

描述符是Python的property类型的实现方式。描述符简单地实现__get____set__等,然后在其定义中添加到另一个类(就像上面对Temperature类所做的那样)。例如:

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

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

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

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

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

编辑:正如jchl在评论中指出的,如果你简单地尝试Temperature.celsiusinstance将会是None

为什么我需要描述符类?

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

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

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

这里的instanceowner是什么?(在__get__)。这些参数的目的是什么?

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

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

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

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

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

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之前,确保你的子类来自object,这将使描述符正确工作,因为得到魔法不适用于旧风格的类。

我试图理解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)的结果。

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

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

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

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

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

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

什么是描述符?

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

  • obj_instance.descriptor调用
    descriptor.__get__(self, obj_instance, owner_class)返回value
    这就是属性上的所有方法和get的工作方式

  • obj_instance.descriptor = value调用
    返回None
    这就是属性上setter的工作方式

  • del obj_instance.descriptor调用
    返回None
    这就是属性上deleter的工作方式

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

A < >强劲数据描述符< / >强__set__和/或__delete__
Non-Data-Descriptor中既没有__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
  • property
  • 一般函数

数据描述符

我们可以看到classmethodstaticmethod是非数据描述符:

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

数据描述符,property

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

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

点查找顺序

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

obj_instance.attribute
  1. 首先,上面的代码查看属性是否是实例类上的Data-Descriptor,
  2. 如果不是,它将查看属性是否在obj_instance__dict__中,则
  3. 它最终会退回到非数据描述符。

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

概述和下一步

我们已经了解到,描述符是具有__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()
  1. 为什么我需要描述符类?

描述符确保该类属性Temperature始终有一个float,并且不能使用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内置(property),并且会被认为更习惯:

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)
  1. 这里的实例和所有者是什么?(在得到)。这些参数的目的是什么?

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

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

下面是一个演示:

>>> 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程序员实现这一结果的预期方式是使用property装饰器,它在底层使用相同的描述符,但将行为带入所有者类的实现中(同样,如上所定义):

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

结论

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

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

为什么我需要描述符类?

灵感来自Buciano Ramalho的流利的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类来进行更常见的验证

你会看到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中的属性查找是如何工作的可能很重要。这假设类没有元类,并且它使用__getattribute__的默认实现(两者都可以用于“自定义”行为)。

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

这表示在Classinstance上查找属性foobar:

enter image description here

这里有两个重要的条件:

  • 如果instance类有一个属性名的条目,并且它有__get____set__
  • 如果instance没有条目作为属性名,但类有一个并且它有__get__

这就是描述符的作用:

  • 数据描述符包含__get____set__
  • 数据描述符只有__get__

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

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

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

为什么我需要描述符类?

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

def test_function(self):
return self


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

如果你在一个实例上查找test_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 ...>

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

函数是数据描述符!

数据描述符的一些内置示例是property。忽略gettersetterdeleterproperty描述符为(来自描述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)

由于它是一个数据描述符,当你查找property的“名称”时,它就会被调用,并且它简单地委托给带有@property@name.setter@name.deleter(如果存在)装饰的函数。

标准库中还有其他几个描述符,例如staticmethodclassmethod

描述符的意义很简单(尽管您很少需要它们):抽象用于属性访问的公共代码。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

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

这里的instanceowner是什么?(在__get__)。这些参数的目的是什么?

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

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

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

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

所以基本上,如果你想在进行类级查找时自定义行为,第三个参数是必要的(因为instanceNone)。

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

你的例子基本上是一个属性,它只允许可以转换为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__的解释,什么是Owner, Instance?

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

  1. __get__ __set__被称为类的描述符,用于工作/保存它们的内部属性,即:__name__(类名/所有者类名),变量- __dict__等。稍后我将解释什么是所有者
  2. 描述符更常用在设计模式中,例如,与装饰符一起使用(将事物抽象出来)。您可以认为它更常用于软件架构设计中,以减少冗余并提高可读性(似乎有些讽刺)。因此,坚持坚实和干燥的原则。
  3. 如果你设计的软件不应该遵循SOLID和DRY原则,你可能不需要它们,但理解它们总是明智的。

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'


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

2. 如何解决?

  • 因为__call__instance作为第一个参数(instance, arg1, arg2),但是什么instance ?

  • Instance是调用描述符类(Method)的主类(MyClass)的实例。所以,instance = MyClass()instance,那么谁是owner呢?然而,在我们的描述符类(Method)中没有方法将其识别为instance。这就是我们需要__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

根据文档,暂时忘记:

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

如果你这样做:instance.method.__get__(instance)

Prints:<__main__.MyClass object at 0x7fb7dd9eab90> <class '__main__.MyClass'>

这意味着MyClass的实例:对象,即instanceOwnerMyClass本身

3.__set__解释:

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

  • < p > (代码中的__ABC0只是更新了描述符的__dict__对象)

  • 现在执行:instance.method = 'value'检查__set__方法中是否设置了value = 'value',我们可以访问描述符method__dict__对象。 做的事: instance.method.__dict__打印:{'_name': 'Internal call', 'value': 'value'}

    或者你可以使用vars(instance.method)检查__dict__的值 打印:{'name': 'Internal call', 'value': 'value'} 我希望事情现在清楚了:)