写在后面
我感觉抓住以下几处重点大略就搞明确这玩意儿了
- 一个描述符是一个有“绑定行为”的对象属性(object attribute),它的访问控制会被形容器协定办法重写。
- 任何定义了
__get__
,__set__
或者__delete__
任一办法的类称为描述符类,其实例对象便是一个描述符,这些办法称为描述符协定。 - 当对一个实例属性进行拜访时,Python 会按
obj.__dict__
→type(obj).__dict__
→type(obj)的父类.__dict__
程序进行查找,如果查找到指标属性并发现是一个描述符,Python 会调用描述符协定来扭转默认的管制行为。 - 描述符是 @property @classmethod @staticmethod 和 super 的底层实现机制。
同时定义了
__get__
和__set__
的描述符称为 数据描述符(data descriptor);仅定义了__get__
的称为 非数据描述符(non-data descriptor) 。两者区别在于:如果obj.__dict__
中有与描述符同名的属性,若描述符是数据描述符,则优先调用描述符,若是非数据描述符,则优先应用obj.__dict__
中属性。描述符协定必须定义在类的档次上,否则无奈被主动调用。翻译原文
原文:Python Descriptors: An Introduction
描述符是Python的一项特定性能,可为语言暗藏的许多魔力提供弱小的反对。如果您已经认为Python描述符是很少应用的高级主题,那么本教程就是帮忙您理解此弱小性能的现实工具。您将理解为什么Python描述符如此乏味,以及在什么状况下应用它们。
在本教程完结时,您将理解:
- 什么是 Python 的描述符
- 它们在 Python 外部应用的中央
- 如何实现本人的描述符
- 何时应用 Python 描述符
本教程实用于中级到高级 Python 开发人员,因为它波及 Python 外部。然而,如果您还没有达到这个程度,那就持续浏览吧!您会找到无关 Python 和 属性查找链的有用信息。
什么是Python描述符?
描述符是实现描述符协定办法的Python对象,当您将其作为其余对象的属性进行拜访时,该描述符使您可能创立具备非凡行为的对象。在这里,您能够看到描述符协定的正确定义:
__get__(self, obj, type=None) -> object__set__(self, obj, value) -> None__delete__(self, obj) -> None__set_name__(self, owner, name)
如果您的描述符仅实现__get__()
,则称其为非数据描述符。如果它实现__set__()
或__delete__()
,则称其为数据描述符。请留神,这种区别不仅在于名称,还在于行为上的区别。这是因为数据描述符在查找过程中具备优先级,这将在前面介绍。
请看以下示例,该示例定义了一个描述符,该描述符在拜访控制台时将其记录在管制台上:
# descriptors.pyclass Verbose_attribute(): def __get__(self, obj, type=None) -> object: print("accessing the attribute to get the value") return 42 def __set__(self, obj, value) -> None: print("accessing the attribute to set the value") raise AttributeError("Cannot change the value")class Foo(): attribute1 = Verbose_attribute()my_foo_object = Foo()x = my_foo_object.attribute1print(x)
在下面的示例中,Verbose_attribute()实现了描述符协定。将其实例化为Foo的属性后,就能够视为描述符。
作为描述符,当应用点表示法拜访时,它具备绑定行为。在这种状况下,每次拜访描述符以获取或设置值时,描述符都会在管制台上记录一条音讯:
- 当拜访
__get__()
值时,它总是返回值42。 - 当拜访
__set__()
的特定值时,它会引发AttributeError异样,这是实现只读描述符的举荐办法。
当初,运行下面的示例,您将看到描述符在返回常量值之前将其记录在管制台上:
$ python descriptors.pyaccessing the attribute to get the value42
在这里,当您尝试拜访attribute1时,描述符依照.__ get __()中的定义将此拜访记录到控制台
描述符在Python外部的工作形式
如果您是具备丰盛的面向对象(开发)教训的Python开发人员,那么您可能会认为上一个示例的办法有些适度。通过应用属性,您能够实现雷同的后果。尽管这是事实,但您可能会诧异地发现Python中的属性也是……描述符!稍后您会看到,属性不是惟一应用Python描述符的性能。
属性中的Python描述符
如果要在不显式应用Python描述符的状况下取得与上一个示例雷同的后果,则最间接的办法是应用 property。以下示例应用 property,该属性在拜访时将信息记录到控制台:
# property_decorator.pyclass Foo(): @property def attribute1(self) -> object: print("accessing the attribute to get the value") return 42 @attribute1.setter def attribute1(self, value) -> None: print("accessing the attribute to set the value") raise AttributeError("Cannot change the value")my_foo_object = Foo()x = my_foo_object.attribute1print(x)
译者注:应用 property 装璜后,name 变成 property 类的一个实例,第二个name 函数应用 name.setter 来装璜,实质是调用 propetry.setter 来产生一个新的 property 实例赋值给第二个 name。第一个 name 和第二个 name 是两个不同 property 实例,但他们都属于同一个描述符类 property。当对 name 赋值时,就会进入property.__set__
,当对 name 取值时,就会进入property.__get__
。
下面的示例应用装璜器来定义属性,然而您可能晓得,装璜器只是语法糖。实际上,后面的示例能够编写如下:
# property_function.pyclass Foo(): def getter(self) -> object: print("accessing the attribute to get the value") return 42 def setter(self, value) -> None: print("accessing the attribute to set the value") raise AttributeError("Cannot change the value") attribute1 = property(getter, setter)my_foo_object = Foo()x = my_foo_object.attribute1print(x)
当初您能够看到该属性是通过应用property()创立的。该函数的签名如下:
property(fget=None, fset=None, fdel=None, doc=None) -> object
property()返回实现描述符协定的属性对象。它应用参数fget,fset和fdel来示意协定的三种办法的理论实现。
办法中的Python描述符
如果您已经用Python编写过面向对象的程序,那么您必定会应用办法。这些惯例函数为对象实例保留第一个参数。应用点表示法拜访办法时,您将调用相应的函数并将对象实例作为第一个参数传递。
将obj.method( args)转换为 method(obj, args)的魔力在于函数对象的__get__()
实现外部,实际上是一个非数据描述符。特地是,该函数对象实现__get__()
,以便在您应用点表示法拜访它时返回一个绑定办法。前面的(* args)通过传递所有须要的额定参数来调用函数。
要理解其工作原理,请看一下官网文档中的这个纯Python示例:
import typesclass Function(object): ... def __get__(self, obj, objtype=None): "Simulate func_descr_get() in Objects/funcobject.c" if obj is None: return self return types.MethodType(self, obj)
在下面的示例中,当应用点符号拜访该函数时,将调用__get__()
并返回一个绑定办法。
这实用于惯例实例办法,同样实用于类办法或静态方法。因而,如果您应用obj.method( args)调用静态方法,则该办法会主动转换为method( args)。同样,如果您应用obj.method(type(obj), args)调用类办法,则该类办法会主动转换为method(type(obj), args)。
在官网文档中,您能够找到一些示例,阐明如果应用纯Python而不是C实现编写如何实现静态方法和类办法。例如,可能的静态方法实现可能是这样的:
class StaticMethod(object): "Emulate PyStaticMethod_Type() in Objects/funcobject.c" def __init__(self, f): self.f = f def __get__(self, obj, objtype=None): return self.f
同样,这可能是可能的类办法实现:
class ClassMethod(object): "Emulate PyClassMethod_Type() in Objects/funcobject.c" def __init__(self, f): self.f = f def __get__(self, obj, klass=None): if klass is None: klass = type(obj) def newfunc(*args): return self.f(klass, *args) return newfunc
请留神,在Python中,类办法只是将类援用作为参数列表的第一个参数的静态方法。
如何应用查找链拜访属性
要理解无关Python描述符和Python外部的更多信息,您须要理解拜访属性时Python中会产生什么。在Python中,每个对象都有一个内置的__dict__
属性。这是一个字典,其中蕴含对象自身中定义的所有属性。要查看实际效果,请思考以下示例:
class Vehicle(): can_fly = False number_of_weels = 0class Car(Vehicle): number_of_weels = 4 def __init__(self, color): self.color = colormy_car = Car("red")print(my_car.__dict__)print(type(my_car).__dict__)
此代码创立一个实例,并打印实例和类的__dict__
属性的内容。当初,运行脚本并剖析输入以查看__dict__
属性集:
{'color': 'red'}{'__module__': '__main__', 'number_of_weels': 4, '__init__': <function Car.__init__ at 0x10fdeaea0>, '__doc__': None}
__dict__
属性集合乎预期。请留神,在Python中一切都是对象。类实际上也是一个对象,因而它还将具备__dict__
属性,其中蕴含该类的所有属性和办法。
那么,当您拜访Python中的属性时,到底产生了什么?让咱们应用前一个示例的批改版本进行一些测试。思考以下代码:
# lookup.pyclass Vehicle(object): can_fly = False number_of_weels = 0class Car(Vehicle): number_of_weels = 4 def __init__(self, color): self.color = colormy_car = Car("red")print(my_car.color)print(my_car.number_of_weels)print(my_car.can_fly)
在此示例中,您将创立一个Car类的实例,Car类继承自Vehicle类。而后,您拜访一些属性。如果运行此示例,则能够看到取得了所有冀望的值:
$ python lookup.pyred4False
在这里,当您拜访实例my_car的属性色彩时,实际上是在拜访对象my_car的__dict__
属性的单个值。当您拜访对象my_car的属性number_of_wheels时,实际上是在拜访Car类的__dict__
属性的单个值。最初,当您拜访can_fly属性时,实际上是在应用Vehicle类的__dict__
属性来拜访它。
这意味着能够重写下面的示例:
# lookup2.pyclass Vehicle(): can_fly = False number_of_weels = 0class Car(Vehicle): number_of_weels = 4 def __init__(self, color): self.color = colormy_car = Car("red")print(my_car.__dict__['color'])print(type(my_car).__dict__['number_of_weels'])print(type(my_car).__base__.__dict__['can_fly'])
在测试这个新示例时,您应该失去雷同的后果:
$ python lookup2.pyred4False
那么,当您应用点符号拜访对象的属性时会产生什么?Python 解释器是如何晓得您的真正需要?好吧,这里有一个叫做查找链的概念:
- 首先,您将从以所要查找的属性命名的数据描述符
__get__
办法返回后果。 - 如果失败,那么您将取得实例对象的
__dict__
值,该值是依据要查找的属性命名的键。 - 如果失败,那么将从以您要查找的属性命名的非数据描述符
__get__
办法中返回后果。 - 如果失败,那么您将取得类型对象的
__dict__
值,该值是依据要查找的属性命名的键。 - 如果失败,那么您将取得父类的
__dict__
值,该值是依据要查找的属性命名的键。 - 如果失败,那么将依照对象的办法解析程序对所有父类反复上一步。
- 如果其余所有操作均失败,则将呈现AttributeError异样。
当初,您明确为什么要晓得描述符是数据描述符还是非数据描述符是如此重要了吧?它们位于查找链的不同档次上,稍后您会发现这种行为上的差别十分不便。
译者注:同时定义了__get__
和__set__
的描述符称为 数据描述符(data descriptor);仅定义了__get__
的称为 非数据描述符(non-data descriptor) 。两者区别在于:如果 obj.__dict__
中有与描述符同名的属性,若描述符是数据描述符,则优先调用描述符,若是非数据描述符,则优先应用 obj.__dict__
中属性。通过类型对象的__dict__
属性拜访,通过父类对象的__dict__
属性拜访。
如何正确应用Python描述符
如果要在代码中应用Python描述符,则只需实现描述符协定。该协定最重要的办法是__get__()
和__set__()
,它们具备以下签名:
__get__(self, obj, type=None) -> object__set__(self, obj, value) -> None
在实现协定时,请记住以下几点:
- self是您正在编写的描述符的实例。
- obj是描述符附加到的对象的实例。
- type是描述符附加到的对象的类型。
在__set__()
中,您没有类型变量,因为您只能在对象上调用__set__()
。相同,您能够在对象和类上都调用__get__()
。
要晓得的另一件事是,每个类仅将Python描述符实例化一次。这意味着蕴含描述符的类的每个实例都共享该描述符实例。这是您可能不会想到的,并且可能导致经典的陷阱,如下所示:
# descriptors2.pyclass OneDigitNumericValue(): def __init__(self): self.value = 0 def __get__(self, obj, type=None) -> object: return self.value def __set__(self, obj, value) -> None: if value > 9 or value < 0 or int(value) != value: raise AttributeError("The value is invalid") self.value = valueclass Foo(): number = OneDigitNumericValue()my_foo_object = Foo()my_second_foo_object = Foo()my_foo_object.number = 3print(my_foo_object.number)print(my_second_foo_object.number)my_third_foo_object = Foo()print(my_third_foo_object.number)
在这里,您有一个Foo类,它定义一个属性number,它是一个描述符。该描述符承受一个数字数值并将其存储在描述符自身的属性中。然而,这种办法行不通,因为Foo的每个实例都共享雷同的描述符实例。您本质上创立的只是一个新的类属性。
尝试运行代码并查看输入:
$ python descriptors2.py333
您能够看到Foo的所有实例都具备雷同的属性编号值,即便最初一个实例是在设置my_foo_object.number属性之后创立的。
那么,如何解决这个问题呢?您可能会认为,最好应用字典来保留描述符所附加的所有对象的所有描述符值。这仿佛是一个不错的解决方案,因为__get__()
和__set__()
具备obj属性,这是您附加到的对象的实例。您能够将此值用作字典的键。
可怜的是,此解决方案有很大的毛病,您能够在以下示例中看到:
# descriptors3.pyclass OneDigitNumericValue(): def __init__(self): self.value = {} def __get__(self, obj, type=None) -> object: try: return self.value[obj] except: return 0 def __set__(self, obj, value) -> None: if value > 9 or value < 0 or int(value) != value: raise AttributeError("The value is invalid") self.value[obj] = valueclass Foo(): number = OneDigitNumericValue()my_foo_object = Foo()my_second_foo_object = Foo()my_foo_object.number = 3print(my_foo_object.number)print(my_second_foo_object.number)my_third_foo_object = Foo()print(my_third_foo_object.number)
在此示例中,您应用字典来存储描述符内所有对象的number属性值。运行此代码时,您会看到它运行失常并且行为合乎预期:
$ python descriptors3.py300
可怜的是,这里的毛病是描述符对所有者对象(描述符附加到的对象实例)放弃强援用。这意味着如果销毁对象,则不会开释内存,因为垃圾收集器会在描述符中查找到对该对象的援用!
您可能认为这里的解决方案可能是应用弱援用。只管可能那样,但您必须面对这样一个事实,即并非所有事物都能够被认为是弱的,并且当您收集对象时,它们会从字典中隐没。
您可能认为这里的解决方案可能是应用弱援用。只管可能那样,但您必须面对这样一个事实,并不是所有类型(tuple,int)都反对弱援用,并且当您收集对象时,它们会从字典中隐没。
最好的解决方案是不将值存储在描述符自身中,而是将它们存储在描述符所附加的对象实例中。接下来尝试这种办法:
# descriptors4.pyclass OneDigitNumericValue(): def __init__(self, name): self.name = name def __get__(self, obj, type=None) -> object: return obj.__dict__.get(self.name) or 0 def __set__(self, obj, value) -> None: obj.__dict__[self.name] = valueclass Foo(): number = OneDigitNumericValue("number")my_foo_object = Foo()my_second_foo_object = Foo()my_foo_object.number = 3print(my_foo_object.number)print(my_second_foo_object.number)my_third_foo_object = Foo()print(my_third_foo_object.number)
在此示例中,当您为对象的number属性设置一个值时,描述符将应用与描述符自身雷同的名称将其存储在所附加对象的__dict__
属性中。 惟一的问题是,在实例化描述符时,必须将名称指定为参数:
number = OneDigitNumericValue("number")
number= OneDigitNumericValue()
是更好的计划吗?可能是,但如果您运行的Python版本低于3.6,您将须要一些带有元类和装璜器的魔法。然而,如果您应用Python 3.6或更高版本,描述符协定具备一个新办法__ set_name __()
,它能够为您实现所有这些魔法,是在 PEP 487提出的:
__set_name__(self, owner, name)
应用此新办法,无论何时实例化描述符,都会调用此办法并主动设置name参数。
当初,尝试为Python 3.6及更高版本重写后面的示例:
# descriptors5.pyclass OneDigitNumericValue(): def __set_name__(self, owner, name): self.name = name def __get__(self, obj, type=None) -> object: return obj.__dict__.get(self.name) or 0 def __set__(self, obj, value) -> None: obj.__dict__[self.name] = valueclass Foo(): number = OneDigitNumericValue()my_foo_object = Foo()my_second_foo_object = Foo()my_foo_object.number = 3print(my_foo_object.number)print(my_second_foo_object.number)my_third_foo_object = Foo()print(my_third_foo_object.number)
当初,已删除__init__()
并实现了__ set_name __()
。这样就能够创立描述符,而无需指定用于存储值的外部属性的名称。您的代码当初看起来也更好更洁净了!
再运行一次此示例,以确保一切正常:
$ python descriptors5.py300
如果您应用的是Python 3.6或更高版本,则此示例应该能够失常运行。
为什么要应用Python描述符
当初,您晓得什么是Python描述符,以及Python自身如何应用它们来反对其某些性能,例如办法和属性。您还理解了如何创立Python描述符,同时防止了一些常见的陷阱。当初所有都应该分明了,然而您可能依然想晓得为什么要应用它们。
以我的教训,我意识许多高级Python开发人员,他们以前从未应用过此性能,也不须要它。这是很失常的,因为在很多状况下都不须要应用Python描述符。然而,这并不意味着Python描述符仅仅是针对高级用户的学术主题。依然有一些很好的用例能够证实学习应用描述符是值得的。
Lazy Properties
第一个也是最间接的示例是惰性属性。这些属性的初始值只有在首次拜访它们时才会加载。而后,他们加载其初始值并保留该值以供当前重用。 思考以下示例。您有一个DeepThought类,其中蕴含一个办法meaning_of_life(),该办法在很久之后会返回一个值:
# slow_properties.pyimport randomimport timeclass DeepThought: def meaning_of_life(self): time.sleep(3) return 42my_deep_thought_instance = DeepThought()print(my_deep_thought_instance.meaning_of_life())print(my_deep_thought_instance.meaning_of_life())print(my_deep_thought_instance.meaning_of_life())
如果您运行此代码并尝试拜访该办法三次,则每三秒钟您将失去一个答案,这是办法外部睡眠工夫的长度。
当初,惰性属性能够在第一次执行此办法时对其进行一次评估。而后,它将缓存后果值,这样,如果再次须要它,就能够立刻取得它。您能够应用Python描述符来实现:
# lazy_properties.pyimport randomimport timeclass LazyProperty: def __init__(self, function): self.function = function self.name = function.__name__ def __get__(self, obj, type=None) -> object: obj.__dict__[self.name] = self.function(obj) return obj.__dict__[self.name]class DeepThought: @LazyProperty def meaning_of_life(self): time.sleep(3) return 42my_deep_thought_instance = DeepThought()print(my_deep_thought_instance.meaning_of_life)print(my_deep_thought_instance.meaning_of_life)print(my_deep_thought_instance.meaning_of_life)
花些工夫钻研此代码并理解其工作原理。您能够在这里看到Python描述符的性能吗?在此示例中,当您应用@LazyProperty装璜器时,mean_of_life将变成LazyProperty的一个实例(跟@property装璜器作用一样)。该描述符将办法及其名称都存储为实例变量。
因为它是一个非数据描述符,所以当您第一次拜访meaning_of_life属性的值时,将主动调用__get__()
并在my_deep_thought_instance对象上执行meaning_of_life()。后果值存储在对象自身的__dict__
属性中。当您再次拜访Meaning_of_life属性时,Python将应用查找链在__dict__
属性中查找该属性的值,并且该值将立刻返回。
译者注 : 实现惰性求值(拜访时才计算,并将值缓存)利用了obj.__dict__
优先级高于 non-data descriptor 的个性第一次调用__get__
以同名属性存于实例字典中,之后就不再调用__get__
请留神,此办法之所以无效,是因为在此示例中,您仅应用了描述符协定的一种办法__get__()
。您只实现了一个非数据描述符。如果您实现了数据描述符,那么该技巧将无奈见效。在查找链之后,它将优先于__dict__
中存储的值。要对此进行测试,请运行以下代码:
# wrong_lazy_properties.pyimport randomimport timeclass LazyProperty: def __init__(self, function): self.function = function self.name = function.__name__ def __get__(self, obj, type=None) -> object: obj.__dict__[self.name] = self.function(obj) return obj.__dict__[self.name] def __set__(self, obj, value): passclass DeepThought: @LazyProperty def meaning_of_life(self): time.sleep(3) return 42my_deep_tought_instance = DeepThought()print(my_deep_tought_instance.meaning_of_life)print(my_deep_tought_instance.meaning_of_life)print(my_deep_tought_instance.meaning_of_life)
在此示例中,您能够看到实现__set__()
当前,即便它基本不执行任何操作,也会创立一个数据描述符。当初,惰性属性的窍门不再起作用。
D.R.Y. Code
描述符的另一个典型用例是编写可重用的代码并使代码 D.R.Y. (DRY准则)Python描述符为开发人员提供了一个杰出的工具,能够编写可在不同属性甚至不同类之间共享的可重用代码。
思考一个示例,其中您具备五个具备雷同行为的不同属性。每个属性只能设置为特定值,即它要么是偶数要么为0:
class Values: def __init__(self): self._value1 = 0 self._value2 = 0 self._value3 = 0 self._value4 = 0 self._value5 = 0 @property def value1(self): return self._value1 @value1.setter def value1(self, value): self._value1 = value if value % 2 == 0 else 0 @property def value2(self): return self._value2 @value2.setter def value2(self, value): self._value2 = value if value % 2 == 0 else 0 @property def value3(self): return self._value3 @value3.setter def value3(self, value): self._value3 = value if value % 2 == 0 else 0 @property def value4(self): return self._value4 @value4.setter def value4(self, value): self._value4 = value if value % 2 == 0 else 0 @property def value5(self): return self._value5 @value5.setter def value5(self, value): self._value5 = value if value % 2 == 0 else 0my_values = Values()my_values.value1 = 1my_values.value2 = 4print(my_values.value1)print(my_values.value2)
如您所见,这里有很多反复的代码。能够应用Python描述符在所有属性之间共享行为。您能够创立一个EvenNumber描述符,并将其用于所有这样的属性:
# properties2.pyclass EvenNumber: def __set_name__(self, owner, name): self.name = name def __get__(self, obj, type=None) -> object: return obj.__dict__.get(self.name) or 0 def __set__(self, obj, value) -> None: obj.__dict__[self.name] = (value if value % 2 == 0 else 0)class Values: value1 = EvenNumber() value2 = EvenNumber() value3 = EvenNumber() value4 = EvenNumber() value5 = EvenNumber()my_values = Values()my_values.value1 = 1my_values.value2 = 4print(my_values.value1)print(my_values.value2)
这段代码看起来好多了!反复项不复存在,当初能够在一个中央实现逻辑,因而,如果须要更改它,则能够轻松实现。
论断
既然您曾经晓得Python如何应用描述符来反对其一些弱小性能,那么您将成为一个更具意识的开发人员,可能理解为什么某些Python性能曾经按这种形式实现。
您曾经理解到:
- 什么是Python描述符以及何时应用它们
- 在Python外部应用描述符的中央
- 如何实现本人的描述符
而且,您当初晓得了一些特定的用例,其中Python描述符特地有用。例如,当您具备必须在许多属性(甚至不同类的属性)之间共享的常见行为时,描述符十分有用。
后记
最近接触到 Python 描述符的概念,官网文档没太看懂,搜了一大圈发现 realpython 这篇文章能够,就顺便翻译过去了,如有翻译不当的中央,欢送拍砖。如果想更深刻地理解Python描述符,请查看官网的Python描述符指南。