Understanding __get__ and __set__ and Python descriptors
我试图理解python的描述符是什么,它们对什么有用。然而,我却在失败。我知道它们是如何工作的,但我的疑问是。请考虑以下代码:
1 2 3 4 5 6 7 8 9 10 11 | 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() |
为什么我需要描述符类?
什么是
我如何调用/使用这个例子?
描述符是如何实现Python的
1 2 | temp=Temperature() temp.celsius #calls celsius.__get__ |
访问您将描述符分配给的属性(在上面的示例中为
您需要使用描述符类来封装驱动它的逻辑。这样,如果描述符用于缓存一些昂贵的操作(例如),那么它可以将值存储在自身而不是类上。
这里有一篇关于描述符的文章。
编辑:正如JCHL在评论中指出的那样,如果你只是简单地尝试
Why do I need the descriptor class?
它为您提供了对属性工作方式的额外控制。例如,如果你已经习惯了Java中的吸气剂和定位器,那么Python就是这样做的。一个优点是它在用户看来就像一个属性(语法没有变化)。因此,您可以从一个普通属性开始,然后,当您需要做一些花哨的事情时,切换到一个描述符。
属性只是一个可变值。描述符允许您在读取或设置(或删除)值时执行任意代码。所以您可以想象使用它将属性映射到数据库中的字段,例如,一种ORM。
另一种用法可能是通过在
What is
instance andowner here? (in__get__ ). What is the purpose of these parameters?
这是非常微妙的(我在这里写一个新的答案的原因-我发现这个问题的同时还想知道同样的事情,但没有发现现有的答案那么好)。
描述符是在类上定义的,但通常是从实例调用的。当从一个实例中调用时,
这只需要用于
How would I call/use this example?
好吧,这里有一个很酷的技巧,使用类似的类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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,您需要确保这些分区是
1 2 | 100.0 32.0 |
现在还有其他更好的方法可以在python中实现相同的效果(例如,如果Celsius是一个属性,这是相同的基本机制,但将所有源都放在temperature类中),但这表明可以做些什么……
I am trying to understand what Python's descriptors are and what they can be useful for.
描述符是具有以下任何特殊方法的类属性(如属性或方法):好的。
__get__ (非数据描述符方法,例如在方法/函数上)__set__ (数据描述符方法,例如在属性实例上)__delete__ (数据描述符法)
这些描述符对象可以用作其他对象类定义的属性。(也就是说,它们生活在类对象的
描述符对象可用于以编程方式管理普通表达式、赋值甚至删除中的点式查找(如
函数/方法、绑定方法、
数据描述符(如
另一个数据描述符是
非数据描述符,通常是实例、类和静态方法,从它们的非数据描述符方法
大多数Python用户只需要学习简单的用法,而不需要进一步学习或理解描述符的实现。好的。深度:描述符是什么?
描述符是具有以下任何方法(
obj_instance.descriptor 调用descriptor.__get__(self, obj_instance, owner_class) 返回value 。这就是所有方法和get 在属性上的工作方式。好的。obj_instance.descriptor = value 调用descriptor.__set__(self, obj_instance, value) 返回None 。这就是setter 在地产上的工作原理。好的。del obj_instance.descriptor 调用descriptor.__delete__(self, obj_instance) 返回None 。这就是deleter 在一个地产上的工作原理。好的。
要用代码定义这个,如果一个对象的属性集与任何所需的属性相交,则该对象就是一个描述符:好的。
1 2 3 4 5 6 | 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)) |
数据描述符具有
1 2 3 4 5 | 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 - 一般功能
非数据描述符
我们可以看到,
1 2 3 4 | >>> is_descriptor(classmethod), is_data_descriptor(classmethod) (True, False) >>> is_descriptor(staticmethod), is_data_descriptor(staticmethod) (True, False) |
两者都只有
1 2 | >>> has_descriptor_attrs(classmethod), has_descriptor_attrs(staticmethod) (set(['__get__']), set(['__get__'])) |
请注意,所有函数也是非数据描述符:好的。
1 2 3 4 | >>> def foo(): pass ... >>> is_descriptor(foo), is_data_descriptor(foo) (True, False) |
数据描述符,
但是,
1 2 3 4 | >>> is_data_descriptor(property) True >>> has_descriptor_attrs(property) set(['__set__', '__get__', '__delete__']) |
点式查找顺序
这些是重要的区别,因为它们会影响点式查找的查找顺序。好的。
1 | obj_instance.attribute |
这种查找顺序的结果是,函数/方法等非数据描述符可以被实例重写。好的。回顾和下一步
我们已经了解到描述符是任何
下面是您的代码,后面是您的问题和答案:好的。
1 2 3 4 5 6 7 8 9 10 | 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() |
Why do I need the descriptor class?
您的描述符确保您总是有一个针对
1 2 3 4 5 | >>> t1 = Temperature() >>> del t1.celsius Traceback (most recent call last): File"<stdin>", line 1, in <module> AttributeError: __delete__ |
否则,描述符将忽略所有者类和所有者实例,而将状态存储在描述符中。您可以使用一个简单的类属性轻松地在所有实例之间共享状态(只要您总是将其设置为类的浮点值,并且从不删除它,或者对代码的用户这样做感到满意):好的。
1 2 | class Temperature(object): celsius = 0.0 |
这将使您获得与示例完全相同的行为(请参见下面问题3的回答),但使用了内置的pythons(
1 2 3 4 5 6 7 8 | 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) |
What is instance and owner here? (in get). What is the purpose of these parameters?
How would I call/use this example?
下面是一个演示:好的。
1 2 3 4 5 6 7 8 9 10 | >>> t1 = Temperature() >>> t1.celsius 0.0 >>> t1.celsius = 1 >>> >>> t1.celsius 1.0 >>> t2 = Temperature() >>> t2.celsius 1.0 |
不能删除属性:好的。
1 2 3 4 | >>> del t2.celsius Traceback (most recent call last): File"<stdin>", line 1, in <module> AttributeError: __delete__ |
您不能分配一个不能转换为浮点的变量:好的。
1 2 3 4 5 | >>> 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程序员实现此结果的预期方式是使用
1 2 3 4 5 6 7 8 | 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | >>> 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 |
结论
我们已经介绍了定义描述符的属性、数据描述符和非数据描述符之间的区别、使用它们的内置对象以及关于使用的特定问题。好的。
那么,你将如何使用这个问题的例子呢?我希望你不会这样做。我希望你从我的第一个建议(一个简单的类属性)开始,如果你觉得有必要的话,继续到第二个建议(属性修饰器)。好的。好啊。
Why do I need the descriptor class?
灵感来源于布西亚诺·拉玛尔霍创作的流畅的Python。
想象你有这样的班级
1 2 3 4 5 6 7 8 9 10 11 | 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 |
我们应该验证权重和价格,避免给它们分配一个负数,如果我们使用描述符作为代理,我们可以写更少的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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:
1 2 3 4 5 6 7 8 | class LineItem(object): weight = Quantity() price = Quantity() def __init__(self, name, weight, price): self.name = name self.weight = weight self.price = price |
我们可以扩展数量类来进行更常见的验证
在详细介绍描述符之前,了解Python中的属性查找是如何工作的可能很重要。这假定类没有元类,并且它使用EDOCX1的默认实现(两者都可以用于"自定义"行为)。好的。
属性查找(在python 3.x或python 2.x中的新样式类)的最佳说明是理解python元类(ionel的代码日志)。图像使用
这表示在
好的。
这里有两个重要条件:好的。
- 如果
instance 的类有一个属性名的条目,并且它有__get__ 和__set__ 。 - 如果
instance 没有属性名的条目,但类有一个条目,并且它有__get__ 。
这就是描述符的由来:好的。
- 同时具有
__get__ 和__set__ 的数据描述符。 - 只有
__get__ 的非数据描述符。
在这两种情况下,返回值都将通过调用
对于类属性查找,查找更加复杂(请参见例如类属性查找(在上面提到的博客中))。好的。
让我们转到您的具体问题:好的。
Why do I need the descriptor class?
Ok.
在大多数情况下,您不需要编写描述符类!不过,您可能是非常普通的最终用户。例如函数。函数是描述符,这就是如何将函数用作方法,并将
1 2 3 4 5 6 | def test_function(self): return self class TestClass(object): def test_method(self): ... |
如果在实例上查找
1 2 3 | >>> instance = TestClass() >>> instance.test_method <bound method TestClass.test_method of <__main__.TestClass object at ...>> |
同样,您也可以通过手动调用函数的
1 2 | >>> test_function.__get__(instance, TestClass) <bound method test_function of <__main__.TestClass object at ...>> |
甚至可以称之为"自绑定方法":好的。
1 2 | >>> test_function.__get__(instance, TestClass)() <__main__.TestClass at ...> |
注意,我没有提供任何参数,并且函数返回了我绑定的实例!好的。
函数是非数据描述符!好的。
数据描述符的一些内置示例是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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) |
因为它是一个数据描述符,所以每当您查找
标准库中还有其他几个描述符,例如
描述符的要点很简单(尽管您很少需要它们):抽象属性访问的公共代码。
另一个例子是类属性。好的。
一个有趣的例子(使用Python3.6中的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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 |
然后可以在类中使用描述符:好的。
1 2 | class Test(object): int_prop = TypedProperty(int) |
和它一起玩:好的。
1 2 3 4 5 6 7 | >>> t = Test() >>> t.int_prop = 10 >>> t.int_prop 10 >>> t.int_prop = 20.0 TypeError: Expected class <class 'int'>, got <class 'float'> |
或"懒惰的财产":好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | 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 |
在这些情况下,将逻辑移动到一个公共描述符中可能是有意义的,但是也可以用其他方法来解决它们(但可能需要重复一些代码)。好的。
What is
instance andowner here? (in__get__ ). What is the purpose of these parameters?Ok.
这取决于如何查找属性。如果查找实例上的属性,则:好的。
- 第二个参数是在其上查找属性的实例
- 第三个参数是实例的类
如果查找类上的属性(假设描述符是在类上定义的):好的。
- 第二个论点是
None 。 - 第三个参数是查找属性的类
因此,基本上,如果您想在进行类级查找时自定义行为,第三个参数是必要的(因为
How would I call/use this example?
Ok.
您的示例基本上是一个属性,它只允许转换为
1 2 3 4 5 6 7 8 9 | >>> 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 |
这就是为什么描述符通常使用第二个参数(
您将看到https://docs.python.org/3/howto/descriptor.html属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | 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 2.7)。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | #!/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) |
结果:
1 2 3 | C:\Users\gkuhn\Desktop>python test2.py <__main__.Celsius instance at 0x02E95A80> 212 |
对于3之前的python,请确保从对象子类,这将使描述符正确工作,因为get magic不适用于旧样式类。