在 Python 教程的前两章中,我们学习了如何使用Python 属性,甚至如何实现自定义属性类。在本章中,您将了解描述符的详细信息。
描述符是在 2.2 版中引入 Python 的。那个时候《Python2.2 的新特性》中提到:“新的类模型背后的一个大思想是,使用描述符来描述对象属性的 API 已经被形式化了。描述符指定了一个属性的值,说明是否它是一个方法或一个字段。使用描述符 API,静态方法和类方法以及更多奇特的构造成为可能。”
描述符是具有“绑定行为”的对象属性,其属性访问已被描述符协议中的方法覆盖。这些方法是__get__()
,__set__()
和__delete__()
。
如果为对象定义了这些方法中的任何一个,则称其为描述符。
它们的目的在于为程序员提供向类添加托管属性的能力。描述符被引入以__dict__
通过上述方法从对象的字典中获取、设置或删除属性。访问类属性将启动查找链。
让我们仔细看看正在发生的事情。假设我们有一个对象obj
:如果我们尝试访问一个属性(或属性)会发生ap
什么?“访问”属性意味着“获取”值,因此该属性用于例如打印函数或表达式内部。无论是obj
和类属于type(obj)
包含字典属性__dict__
。这种情况如下图所示:
obj.ap
有一个以 开头的查找链obj.__dict__['ap']
,即检查是否obj.ap
是字典的键obj.__dict__['ap']
。
如果ap
不是 的键obj.__dict__
,它将尝试查找type(obj).__dict__['ap']
。
如果obj
也没有包含在这个字典中,它会继续检查type(ap)
排除元类的基类。
我们在一个例子中证明了这一点:
类 A : ca_A = "A 的类属性" def __init__ ( self ): self 。ia_A = "A 实例的实例属性" class B ( A ): ca_B = "B 的类属性" def __init__ ( self ): super () 。__init__ () 自我。ia_B = "A 实例的实例属性" x = B () print ( x . ia_B) 打印( x . ca_B ) 打印( x . ia_A ) 打印( x . ca_A )
输出:
实例的实例属性 B的类属性 实例的实例属性 A的类属性
如果我们调用,print(x.non_existing)
我们会得到以下异常:
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-4-119192d61d5e> in <module>
----> 1 print(x.non_existing)
AttributeError: 'B' object has no attribute 'non_existing'
如果查找的值是定义描述符方法之一的对象,则 Python 可能会覆盖默认行为并改为调用描述符方法。这在优先链中发生的位置取决于定义了哪些描述符方法。
描述符提供了一个强大的通用协议,它是属性、方法、静态方法、类方法和 super() 背后的底层机制。需要描述符来实现 2.2 版中引入的所谓新样式类。“新样式类”是当今的默认设置。
描述符协议
通用描述符协议由三种方法组成:
descr.__get__(self, obj, type=None) -> value
descr.__set__(self, obj, value) -> None
descr.__delete__(self, obj) -> None
如果您定义了这些方法中的一个或多个,您将创建一个描述符。我们区分数据描述符和非数据描述符:
非数据描述符
如果我们只定义 __get__() 方法,我们就创建了一个非数据描述符,主要用于方法。
数据描述符
如果一个对象定义了 __set__() 或 __delete__(),它被认为是一个数据描述符。要创建只读数据描述符,请使用 __set__() 定义 __get__() 和 __set__() 并在调用时引发 AttributeError 。使用异常引发占位符定义 __set__() 方法足以使其成为数据描述符。
现在我们终于来到了下面代码中的简单描述符示例:
class SimpleDescriptor ( object ): """ 一个可以设置和返回值的简单数据描述符 """ def __init__ ( self , initval = None ): print ( "__init__ of SimpleDecorator call with initval:" , initval ) self . __set__ ( self , initval ) def __get__ ( self , instance , owner ): print ( instance , owner) print ( 'Getting (Retrieving) self.val:' , self . val ) return self . val def __set__ ( self , instance , value ): print ( 'Setting self.val to ' , value ) self 。val = value class MyClass ( object ): x = SimpleDescriptor ( "green" ) m = MyClass () 打印(米。x ) 米。x = "黄色" 打印( m . x )
输出:
用 initval 调用 SimpleDecorator 的 __init__:green 将 self.val 设置为绿色 <__main__.MyClass 对象在 0x7efe93ff4a20> <class '__main__.MyClass'> 获取(检索)self.val:绿色 绿色 将 self.val 设置为黄色 <__main__.MyClass 对象在 0x7efe93ff4a20> <class '__main__.MyClass'> 获取(检索)self.val:黄色 黄色的
第三个参数owner
的__get__
永远是所有者类,并为用户提供了一个选项,做一些与被用来调用描述符的类。通常,即如果描述符是通过对象调用的obj
,则可以通过调用来推断对象类型type(obj)
。如果描述符是通过类调用的,情况就不同了。在这种情况下,None
除非给出第三个参数,否则不可能访问该类。第二个参数instance
是访问属性的实例,或者None
是通过所有者访问属性的时间。
让我们看看__dict__
实例和类的字典:
'x' 是上一个类中的一个类属性。您可能会问自己,我们是否也可以在__init__
方法中使用这种机制来定义实例属性。这不可能。方法__get__()
,__set__()
和__delete__()
仅当包含该方法的类的实例(所谓的描述符类)出现在所有者类中时才适用(描述符必须在所有者的类字典中或在其一个的类字典中)父母)。在上面的示例中,属性“x”位于__dict__
所有者类 MyClass 的所有者中,如下所示:
打印(米。__dict__ ) 打印(MyClass的。__dict__ ) 打印(SimpleDescriptor 。__dict__ )
输出:
{} {'__module__': '__main__', 'x': <__main__.SimpleDescriptor object at 0x7efe93ff49e8>, '__dict__': <attribute '__dict__' of 'MyClass' objects>, '__weakref__': <attribute '__weakref__' of 'MyClass' '对象>,'__doc__':无} {'__module__': '__main__', '__doc__': '\n 一个可以设置和返回值的简单数据描述符\n ', '__init__': <function SimpleDescriptor.__init__ at 0x7efe93fc8d08>, '__get__': <function SimpleDescriptor .__get__ at 0x7efe93fc8840>, '__set__': <function SimpleDescriptor.__set__ at 0x7efe93fc88c8>, '__dict__': <attribute '__dict__' of 'SimpleDescriptor' objects>, '__weaktribute objects __' of <weakSimpleDescriptor __': }
打印( MyClass . __dict__ [ 'x' ])
输出:
<__main__.SimpleDescriptor 对象在 0x7efe93ff49e8>
可以通过其方法名称直接调用描述符,例如d.__get__(obj)
。
或者,更常见的是在属性访问时自动调用描述符。例如,在 的字典中obj.d
查找。如果定义了方法,则根据下面列出的优先级规则调用。d
__dict__
obj
d
__get__()
d.__get__(obj)
如果obj
是对象或类,则有所不同:
-
对于对象,控制调用的方法是在
object.__getattribute__()
其中转换b.x
为调用type(b).__dict__['x'].__get__(b, type(b))
。该实现通过优先级链进行工作,该优先级链赋予数据描述符高于实例变量的优先级,实例变量优先于非数据描述符的优先级,并为__getattr__()
提供的情况分配最低优先级。 -
对于类,相应的方法在
type
类中,即将type.__getattribute__()
Bx 转换为B.__dict__['x'].__get__(None, B)
。
__getattribute__
不是用 Python 实现的,而是用 C 实现的。 下面的 Python 代码是在 Python 中的逻辑模拟。我们可以看到描述符被__getattribute__
实现调用。
def __getattribute__ ( self , key ): “在 Objects/typeobject.c 中模拟 type_getattro()” v = type 。__getattribute__ ( self , key ) 如果 hasattr ( v , '__get__' ): 返回 v 。__get__ ( None , self ) 返回 v m 。__getattribute__ ( "x" )
输出:
<__main__.MyClass 对象在 0x7efe93ff4a20> <class '__main__.MyClass'> 获取(检索)self.val:黄色
返回的对象super()
也有__getattribute__()
调用描述符的自定义方法。属性查找紧随其后super(B, obj).m
搜索obj.__class__.__mro__
基类,然后返回。如果不是描述符,则原样返回。如果不在字典中,则使用恢复搜索。A
B
A.__dict__['m'].__get__(obj, B)
m
m
object.__getattribute__()
上面的细节表明描述符的机制嵌入在__getattribute__()
object、type 和 super() 的方法中。当类从对象派生或具有提供类似功能的元类时,类会继承此机制。这也意味着可以通过覆盖__getattribute__()
.
from weakref import WeakKeyDictionary class Voter : required_age = 18 # 在德国 def __init__ ( self ): self . age = WeakKeyDictionary () def __get__ ( self , instance_obj , objtype ): 返回 self 。年龄。get ( instance_obj ) def __set__ ( self , instance , new_age ): 如果 new_age < Voter 。required_age : 味精 = ' {name}的不老足以在德国投票' 加薪 异常(MSG 。格式(名称=实例。名称)) 的自我。age [ instance ] = new_age print ( ' {name} can vote in Germany' . format ( name = instance . name )) def __delete__ ( self , instance ): del self 。年龄[实例] 类 人: voter_age = Voter () def __init__ ( self , name , age ): self 。姓名 = 姓名 自我。voter_age = 年龄 p1 = Person ( 'Ben' , 23 ) p2 = Person ( 'Emilia', 22 ) p2 。选民年龄
输出:
本可以在德国投票 艾米利亚可以在德国投票
property() 类的纯示例实现(python.org 的描述符)
类 属性: “在 Objects/descrobject.c 中模拟 PyProperty_Type()” def __init__ ( self , fget = None , fset = None , fdel = None , doc = None ): self 。fget = fget self 。fset = fset self 。FDEL = FDEL 如果 文档 是 无 和 fget 是 不 无: DOC = fget 。__doc__ 自我。__doc__ = DOC DEF __get__ (自, OBJ , OBJTYPE =无): 如果 OBJ 是 无: 返回 自 如果 自我。fget is None : raise AttributeError ( "unreadable attribute" ) return self 。fget ( obj ) def __set__ ( self , obj , value ): 如果 self 。fset 为 None : 引发 AttributeError ( "can't set attribute" ) self 。fset ( obj , value ) def __delete__ ( self , obj ): if self . fdel 是 None : 引发 AttributeError ( "can't delete attribute" ) self 。fdel ( obj ) def 吸气剂(自, fget ): 返回 类型(自)(fget , 自我。FSET , 自我。FDEL , 自我。__doc__ ) DEF 设定器(自, FSET ): 返回 类型(自)(自我。fget , FSET , 自我。FDEL , self . __doc__ ) def 删除器( self , fdel ): 返回 类型( self )( self . fget , self . fset , fdel , self . __doc__ )
一个使用我们的 Property 实现的简单类。
A类: def __init__ ( self , prop ): self 。prop = prop @Property def prop ( self ): print ( "现在将返回属性'prop':" ) return self 。__prop @prop 。setter def prop ( self , prop ): print ( "prop will be set" ) self . __prop = 道具
使用我们的 A 类:
print ( "用值'Python'初始化属性'prop'" ) x = A ( "Python" ) print ( "值是:" , x . prop ) print ( "将属性'prop'重新分配给'Python描述符'" ) x 。丙 = “Python的描述符” 打印(“的值是:” , X 。道具)
输出:
使用值 'Python' 初始化属性 'prop' 道具将被设置 现在将返回属性“prop”: 值为:Python 将属性“prop”重新分配给“Python 描述符” 道具将被设置 现在将返回属性“prop”: 值为:Python 描述符
让我们用一些打印函数让我们的 Property 实现更健谈,看看发生了什么:
类 属性: “在 Objects/descrobject.c 中模拟 PyProperty_Type()” def __init__ ( self , fget = None , fset = None , fdel = None , doc = None ): print ( " \n __init__ of Property 调用:" ) 打印( "fget=" + str ( fget ) + " fset=" + str ( fset ) + \ " fdel=" + str ( fdel ) + " doc=" + str ( doc )) self 。fget = fget self 。fset = fset self 。FDEL = FDEL 如果 文档 是 无 和 fget 是 不 无: DOC = fget 。__doc__ 自我。__doc__ = doc def __get__ ( self , obj, objtype = None ): print ( " \n Property.__get__ has been called!" ) if obj is None : return self if self 。fget is None : raise AttributeError ( "unreadable attribute" ) return self 。fget ( obj ) def __set__ ( self , obj , value ): 打印( " \n物业.__ set__被称为“! ), 如果 自我。FSET 是 无: 提高 AttributeError的(”不能设置属性“ ) 的自我。FSET (OBJ , 值) 高清 __delete__ (自我, OBJ ): 打印(” \ n属性。 !__delete__被称为” ) ,如果 自我。FDEL 是 无: 提高 AttributeError的(‘不能删除属性’ ) 自我。fdel ( obj ) def getter ( self , fget ): print ( " \n Property.getter has been called!" ) return type ( self )( fget , self . fset , self . fdel , self . __doc__ ) def setter ( self , fset ): print ( " \n Property.setter 已被调用!" ) 返回 类型(个体经营)(自我。fget , FSET , 自我。FDEL , 自我。__doc__ ) DEF 缺失者(个体经营, FDEL ): 打印(“ \ n Property.deleter被称为!” ) 的返回 类型(个体经营)(自我。fget 、 self . fset 、 fdel 、 self . __doc__ ) 类 A : def __init__ ( self , prop ): self 。prop = prop @Property def prop ( self ): """ 这将是属性 """ print ( "The Property 'prop' will be returned now:" ) return self的文档字符串。__prop @prop 。setter def prop ( self , prop ): print ( "prop will be set" ) self . __prop = prop def prop2_getter ( self ): 返回 self 。__prop2 def prop2_setter ( self , prop2 ): self 。__prop2 = PROP2 PROP2 = 属性(prop2_getter , prop2_setter ) 打印(“初始化属性'道具'同值'的Python'” ) X = 甲(“Python的” ) 的打印(“的值是:” , X 。丙) 打印( "将属性 'prop' 重新分配给 'Python 描述符'" ) x . 丙 = “Python的描述符” 打印(“的值是:” , X 。道具) 打印(甲。道具。吸气剂(X )) DEF new_prop_setter (自, 丙): 如果 丙== “foo”的: 自我。__prop = "foobar" else : self 。__prop = 道具 A 。道具。二传手
输出:
__init__ 的属性调用: fget=<function A.prop at 0x7efe93fff840> fset=None fdel=None doc=None Property.setter 已被调用! __init__ 的属性调用: fget=<function A.prop at 0x7efe93fff840> fset=<function A.prop at 0x7efe9370f840> fdel=None doc= 这将是属性的文档字符串 __init__ 的属性调用: fget=<function A.prop2_getter at 0x7efe93715048> fset=<function A.prop2_setter at 0x7efe93715158> fdel=None doc=None 使用值 'Python' 初始化属性 'prop' Property.__set__ 已被调用! 道具将被设置 Property.__get__ 已被调用! 现在将返回属性“prop”: 值为:Python 将属性“prop”重新分配给“Python 描述符” Property.__set__ 已被调用! 道具将被设置 Property.__get__ 已被调用! 现在将返回属性“prop”: 值为:Python 描述符 Property.__get__ 已被调用! Property.getter 已被调用! __init__ 的属性调用: fget=<__main__.A object at 0x7efe9371b080> fset=<function A.prop at 0x7efe9370f840> fdel=None doc= 这将是属性的文档字符串 <__main__.Property 对象在 0x7efe93ff4358> Property.__get__ 已被调用!
机器人类: def __init__ ( self , name = "Marvin" , city = "Freiburg" ): self 。姓名 = 姓名 自我。city = city @Property def name ( self ): 返回 self 。__name @name 。setter def name ( self , name ): if name == "hello" : self. __name = "hi" else : self . __name = name x = Robot ( "Marvin" ) 打印( x . name ) x . name = "Eddie" 打印( x . name )
输出:
__init__ 的属性调用: fget=<function Robot.name at 0x7efe93715ea0> fset=None fdel=None doc=None Property.setter 已被调用! __init__ 的属性调用: fget=<function Robot.name at 0x7efe93715ea0> fset=<function Robot.name at 0x7efe93715400> fdel=None doc=None Property.__set__ 已被调用! Property.__get__ 已被调用! 马文 Property.__set__ 已被调用! Property.__get__ 已被调用! 埃迪
A类: def a ( func ): def helper ( self , x ): return 4 * func ( self , x ) return helper @a def b ( self , x ): return x + 1 a = A () a 。乙( 4 )
输出:
20
很多人问,是否可以在运行时自动创建描述符。正如我们在以下示例中所示,这是可能的。另一方面,这个例子不是很有用,因为 getter 和 setter 没有真正的功能:
class DynPropertyClass ( object ): def add_property ( self , attribute ): """ 向类中添加一个属性 """ def get_attribute ( self ): """ 将检索属性 'attribute' 的值 """ return getattr ( self , "_" + type ( x ) . __name__ + "__" + attribute ) def set_attribute ( self , 值): """ 将检索属性 'attribute' 的值 """ #setter = lambda self, value: self.setProperty(attribute, value) setattr ( self , "_" + type ( x ) . __name__ + "__" + attribute , value ) #构造property 属性并将其添加到类 setattr ( type ( self ), attribute , property ( fget = get_attribute , fset = set_attribute , 文档=“自动生成的方法” )) x = DynPropertyClass () x 。add_property ( 'name' ) x 。add_property ( 'city' ) x 。姓名 = "亨利" x 。姓名 x 。city = "Hamburg" 打印( x . name , x . city ) 打印( x . __dict__ )
输出:
亨利·汉堡 {'_DynPropertyClass__name':'亨利','_DynPropertyClass__city':'汉堡'}