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