问题:了解__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()
为什么需要描述符类?
什么是
instance
和owner
这里?(在中__get__
)。这些参数的目的是什么?我将如何调用/使用此示例?
回答 0
描述符是如何实现Python property
类型的。描述符仅实现__get__
,__set__
等等,然后被添加到其定义中的另一个类中(就像上面对Temperature类所做的那样)。例如:
temp=Temperature()
temp.celsius #calls celsius.__get__
访问您为描述符分配的属性(celsius
在上面的示例中)调用适当的描述符方法。
instance
in __get__
是类的实例(因此,上面__get__
将接收temp
,owner
而是带有描述符的类(因此将是Temperature
)。
您需要使用描述符类来封装为其提供动力的逻辑。这样,如果描述符用于缓存某些昂贵的操作(例如),它可以将值存储在自身而不是其类上。
关于描述符的文章可以在这里找到。
编辑:正如jchl在评论中指出的,如果您只是尝试Temperature.celsius
,instance
将是None
。
回答 1
为什么需要描述符类?
它使您可以更好地控制属性的工作方式。例如,如果您习惯于使用Java中的getter和setter,那么这就是Python的方法。优点之一是,它对用户的外观就像一个属性(语法没有变化)。因此,您可以从一个普通属性开始,然后在需要做一些花哨的事情时,切换到一个描述符。
属性只是可变值。描述符使您可以在读取或设置(或删除)值时执行任意代码。因此,您可以想象使用它将属性映射到数据库中的字段,例如–一种ORM。
另一个用法可能是通过抛出异常来拒绝接受新值,__set__
从而有效地使“属性” 变为只读。
什么是
instance
和owner
这里?(在中__get__
)。这些参数的目的是什么?
这是相当微妙的(这也是我在这里写一个新答案的原因-我在想这个问题的同时发现了这个问题,却没有发现现有的答案那么好)。
描述符是在类上定义的,但通常是从实例中调用的。当同时从实例instance
和实例都调用它时owner
(并且可以从中进行计算owner
,instance
因此似乎没有意义)。但是,当从类中调用时,仅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类中),但这显示了可以完成的工作…
回答 2
我试图了解什么是Python的描述符以及它们可用于什么。
描述符是具有以下任何特殊方法的类属性(如属性或方法):
__get__
(非数据描述符方法,例如方法/函数)__set__
(数据描述符方法,例如在属性实例上)__delete__
(数据描述符方法)
这些描述符对象可用作其他对象类定义的属性。(也就是说,它们位于__dict__
类对象的中。)
描述符对象可用于以编程方式管理foo.descriptor
正则表达式,赋值甚至删除中的点分查找(例如)的结果。
函数/方法,绑定方法,property
,classmethod
和staticmethod
所有使用这些特殊的方法来控制它们是如何通过点查找访问。
像这样的数据描述符property
可以根据对象的简单状态对属性进行延迟评估,与实例中预先计算每个可能的属性相比,允许实例使用更少的内存。
member_descriptor
创建的另一个数据描述符a __slots__
通过允许类将数据存储在可变的类似元组的数据结构中而不是更灵活但占用空间的方法来节省内存__dict__
。
非数据描述符(通常是实例,类和静态方法)从其非数据描述符方法中获取其隐式第一个参数(通常分别命名为cls
和self
)__get__
。
大多数Python用户只需要学习简单的用法,而无需进一步学习或理解描述符的实现。
深入:什么是描述符?
描述符是具有以下任何一种方法(__get__
,__set__
或__delete__
)的对象,旨在通过点分查找来使用,就好像它是实例的典型属性一样。对于obj_instance
具有一个descriptor
对象的所有者对象,:
obj_instance.descriptor
调用descriptor.__get__(self, obj_instance, owner_class)
返回a。value
这就是所有方法和get
on属性的工作方式。obj_instance.descriptor = value
调用descriptor.__set__(self, obj_instance, value)
返回None
这就是setter
on属性的工作方式。del obj_instance.descriptor
调用descriptor.__delete__(self, obj_instance)
返回None
这就是deleter
on属性的工作方式。
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
property
- 一般功能
非数据描述符
我们可以看到,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)
数据描述符 property
但是,property
是一个数据描述符:
>>> 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()
- 为什么需要描述符类?
您的描述符可确保您始终为的此类属性具有浮点数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内置(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)
- 什么是实例和所有者?(获得)。这些参数的目的是什么?
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程序员完成此结果的预期方式是使用property
装饰器,该装饰器在幕后使用相同的描述符,但将行为带入了owner类的实现(同样,如上所定义):
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
结论
我们已经介绍了定义描述符的属性,数据描述符和非数据描述符之间的区别,使用它们的内置对象以及有关使用的特定问题。
同样,您将如何使用问题的示例?我希望你不会。我希望您从我的第一个建议(一个简单的类属性)开始,如果有必要,请继续进行第二个建议(属性装饰器)。
回答 3
在详细介绍描述符之前,了解Python中的属性查找如何工作可能很重要。这假定该类没有元类,并且使用的默认实现__getattribute__
(均可用于“自定义”行为)。
在这种情况下,属性查找(在Python 3.x中或在Python 2.x中用于新样式类)的最佳说明来自于了解Python元类(ionel的代码日志)。该图像:
代替“不可自定义的属性查找”。
这代表一个属性的查找foobar
上instance
的Class
:
这里有两个条件很重要:
- 如果的类
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
。忽略getter
,setter
和deleter
,property
描述符是(来自描述符方法指南“属性”):
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
(如果存在的话)。
还有一些其他的描述符在标准库中,例如staticmethod
,classmethod
。
描述符的要点很容易(尽管您很少需要它们):用于属性访问的抽象通用代码。property
是实例变量访问function
的抽象,staticmethod
提供方法的抽象,为不需要实例访问classmethod
的方法提供抽象,为需要类访问而不是实例访问的方法提供抽象(这有点简化)。
另一个示例是class属性。
一个有趣的示例(__set_name__
从Python 3.6 使用)也可以是仅允许特定类型的属性:
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
在这些情况下,将逻辑移到公共描述符中可能很有意义,但是也可以使用其他方法解决它们(但可能需要重复一些代码)。
什么是
instance
和owner
这里?(在中__get__
)。这些参数的目的是什么?
这取决于您如何查找属性。如果您在实例上查找属性,则:
- 第二个参数是您在其中查找属性的实例
- 第三个参数是实例的类
如果您在类上查找属性(假设描述符是在类上定义的):
- 第二个参数是
None
- 第三个参数是您在其中查找属性的类
因此,基本上,如果要在执行类级查找时自定义行为(因为instance
is None
),则第三个参数是必需的。
我将如何调用/使用此示例?
您的示例基本上是一个属性,该属性仅允许将值转换为该值,float
并且该值可以在该类的所有实例之间共享(并且可以在该类上共享-尽管只能在该类上使用“读取”访问权限,否则您将替换描述符实例):
>>> 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
)存储值以避免共享它的原因。但是在某些情况下,可能需要在实例之间共享一个值(尽管目前我无法想到一种情况)。但是,对于温度等级的摄氏温度特性几乎没有任何意义……除了纯粹作为学术练习之外。
回答 4
为什么需要描述符类?
由Buciano Ramalho的Fluent Python启发
想象你有一个这样的Class
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类以进行更常见的验证
回答 5
我尝试(根据建议进行了一些小的更改)安德鲁·库克答案中的代码。(我正在运行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
对于3之前的Python,请确保您从对象继承了子类,这将使描述符正确工作,因为get魔术不适用于旧样式类。
回答 6
您会看到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__)