本文共 8041 字,大约阅读时间需要 26 分钟。
#简介
通常,描述符是具有 ” 绑定行为 “ 的对象属性,其属性访问被描述符协议中的方法重写。这些方法是__get__()
,__set__()
和__delete__()
。如果对象中定义了这些方法中的任何一种,则称它为描述符。
属性访问的默认行为是从对象的字典中获取,设置或删除属性。例如,a.x
有一个以a.__dict__['x']
开始的查找链,接着是type(a).__dict__['x']
,然后是除了元类之外的type(a)
的基类。如果查找的值是定义其中一个描述符方法的对象,则Python可以调用描述符方法重写默认行为。在优先链中发生这种情况取决于定义了哪些描述符方法。请注意,描述符仅针对new style对象或类进行调用(如果类继承自object
或type
,则该类为new style)。
描述符是一个强大的通用协议。它们是属性,方法,静态方法,类方法和super()
的工作机制。它们被用于整个Python本身,以实现版本2.2中引入的new style类。描述符简化了底层C代码,为日常Python程序提供了一套灵活的新工具。
#描述符协议
descr.__get__(self, obj, type=None) --> value
descr.__set__(self, obj, value) --> None
descr.__delete__(self, obj) --> None
这就是所有的描述符方法。定义这些方法中的任何一个,就将对象视为描述符,并且可以在查找属性时重写默认行为。
如果一个对象同时定义了__get__()
和__set__()
,它就被认为是一个数据描述符。只定义__get__()
的描述符被称为非数据描述符(它们通常用于方法,但其他用途也是可能的)。
数据和非数据描述符不同在于它们和实例字典的优先级。如果实例的字典中有一个与数据描述符同名的条目,则数据描述符优先。如果实例的字典中有一个与非数据描述符名称相同的条目,则字典条目优先。
要创建一个只读数据描述符,同时定义__get __
和__set__
,并且定义__set__
时引发AttributeError异常。
#调用描述符
描述符可以通过其方法名直接调用。例如,d.__get__(obj)
。
另外,更为常见的是描述符在属性访问时被自动调用。例如,obj.d
在obj
的字典中查找d
。如果d
定义了方法__get__()
,则根据下面列出的优先规则d.__get__(obj)
被调用。
调用的细节取决于obj
是一个对象还是一个类。无论哪种方式,描述符只适用于new style对象和类。如果一个类是object
的一个子类,它就是new style。
对于对象,机关在object.__getattribute__()
中,通过object.__getattribute__()
将b.x
转换成(b).__dict__['x'].__get__(b,type(b))
。该实现通过优先级链工作,该优先级链说明数据描述符优先于实例变量,实例变量优先于非数据描述符,并且如果提供了__getattr__()
,则赋予其最低优先级。完整的C实现可以在Objects/object.c
中的PyObject_GenericGetAttr()
中找到。
对于类,机关在type.__getattribute__()
中,它将B.x
转换为B.__dict__['x'].__get__(None,B)
。在纯Python中,它看起来像:
def __getattribute__(self, key): "Emulate type_getattro() in Objects/typeobject.c" v = object.__getattribute__(self, key) if hasattr(v, '__get__'): return v.__get__(None, self) return v
需要记住的重点是:
- 描述符由
__getattribute__()
方法调用 - 覆盖
__getattribute__()
可防止描述符自动调用 __getattribute__()
仅适用于new style类和对象object.__getattribute__()
和type.__getattribute__()
对__get__()
进行不同的调用。- 数据描述符总是重写实例字典。
- 非数据描述符可能被实例字典重写。
super()
返回的对象同样具有自定义__getattribute__()
方法用于调用描述符。调用super(B,obj).m()
会立即搜索obj.__class__.__mro__
中B
之后的基类A
,然后返回A.__dict__['m'].__get__(obj,B)
。如果不是描述符,则原样返回m
。如果不在字典中,m
会继续调用object.__getattribute__()
。
注意,在Python 2.2中,如果m
是一个数据描述符,super(B,obj).m()
只会调用__get__()
。在Python 2.3中,除非涉及old style类,否则非数据描述符也会调用。实现细节位于Objects/typeobject.c
中的super_getattro()
中。
上面的细节表明描述符的机制嵌入在object
,type
和super()
的__getattribute__()
方法中。当它们从object
派生时,或者它们具有提供类似功能的元类时,类继承此机制。同样,类可以通过重写__getattribute__()
来关闭描述符调用。
#示例
以下代码创建一个类,其对象是数据描述符,这个描述符对于监视几个属性非常有用:
class RevealAccess(object): """A data descriptor that sets and returns values normally and prints a message logging their access. """ def __init__(self, initval=None, name='var'): self.val = initval self.name = name def __get__(self, obj, objtype): print 'Retrieving', self.name return self.val def __set__(self, obj, val): print 'Updating', self.name self.val = val>>> class MyClass(object):... x = RevealAccess(10, 'var "x"')... y = 5...>>> m = MyClass()>>> m.xRetrieving var "x"10>>> m.x = 20Updating var "x">>> m.xRetrieving var "x"20>>> m.y5
协议很简单,并提供令人兴奋的可能性。几个用例非常常见,它们已被封装到单个函数调用中。属性,绑定和未绑定方法,静态方法和类方法都是基于描述符协议。
属性
调用property()
是一种简洁的构建数据描述符的方法,可以在访问属性时触发函数调用。函数原型是:
property(fget=None, fset=None, fdel=None, doc=None) -> property attribute
下面例子展示了一个典型应用:定义托管属性x
class C(object): def getx(self): return self.__x def setx(self, value): self.__x = value def delx(self): del self.__x x = property(getx, setx, delx, "I'm the 'x' property.")
为了了解property()
是如何通过描述符协议实现的,下面是一个纯Python实现:
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__)
只要用户界面授予了属性访问权,然后后续更改需要干预的方法,property()
内置函数就会有所帮助。
例如,电子表格类可以通过Cell('b10').value
对单元格值进行访问。对这个程序的后续改进需要在每次访问时重新计算单元格; 然而,程序员不想影响直接访问属性的现有客户端代码。 解决方案是对value
属性的访问控制包装在属性数据描述符中:
class Cell(object): . . . def getvalue(self): "Recalculate the cell before returning value" self.recalc() return self._value value = property(getvalue)
函数与方法
Python的面向对象功能建立在基于功能的环境之上。使用非数据描述符可以将两者无缝连接。
类字典将方法作为函数存储。在类定义中,方法使用def
和lambda
编写,这是创建函数的常用工具。与常规函数的唯一区别是第一个参数是为对象实例保留的。 按照Python约定,实例引用称为self
,但也可以称为this
或任何其他变量名称。
为了支持方法调用,函数在属性访问期间包含用于绑定方法的__get__()
方法。 这意味着所有函数都是非数据描述符,它们返回绑定或未绑定的方法,具体取决于它们是从对象还是类调用。 在纯Python中,它的工作原理是这样的:
class Function(object): . . . def __get__(self, obj, objtype=None): "Simulate func_descr_get() in Objects/funcobject.c" return types.MethodType(self, obj, objtype)
运行解释器显示了函数描述符在实际中的工作方式:
>>> class D(object):... def f(self, x):... return x...>>> d = D()>>> D.__dict__['f'] # Stored internally as a function>>> D.f # Get from a class becomes an unbound method >>> d.f # Get from an instance becomes a bound method >
输出结果表明绑定和未绑定的方法是两种不同的类型。 它们是在通过C
实现的Objects/classobject.c
文件中的PyMethod_Type
对象,这个对象具有两个不同的表示,具体取决于im_self
字段是否设置或NULL
(在C
中等效于None
)。
同样,调用方法对象的效果依赖于im_self
字段。 如果设置(意思是绑定),则将原始函数(存储在im_func
字段中)调用,并将第一个参数设置为实例。 如果未绑定,则所有参数都将不变地传递给原始函数。 instancemethod_call()
的实际C
实现稍微复杂一些,因为它包含一些类型检查。
静态方法和类方法
非数据描述符为将函数绑定成方法这种常用模式提供了一种简单的机制。
回顾一下,函数有一个__get__()
方法,以便在属性访问时可以将函数转换为方法。非数据描述符将obj.f(* args)
调用转换为f(obj,* args)
。将klass.f(* args)
调用变成f(* args)
。
该图表总结了绑定及其两个最有用的变体:
Transformation | Called from an Object | Called from a Class |
---|---|---|
function | f(obj, *args) | f(*args) |
staticmethod | f(*args) | f(*args) |
classmethod | f(type(obj), *args) | f(klass, *args) |
静态方法原样返回函数。调用c.f
或C.f
相当于object.__ getattribute__(c,"f")
或object.__getattribute__(C,"f")
。因此,函数可以从对象或类中以相同的方式访问。
不引用self
的方法适合成为静态方法。
例如,一个统计软件包可能包含实验数据的容器类。该类提供了用于计算数据的平均值,中值和其他描述性统计量。但是,可能有一些有用的功能在概念上是相关的,但不依赖于数据。例如,erf(x)
是一个经常用到的例子,它出现在统计工作中,但不依赖于特定的数据集。它可以从对象或类中调用:s.erf(1.5)-> .9332
或Sample.erf(1.5)-> .9332
。
由于静态方法将函数原样返回,因此下面的例子并不奇怪:
>>> class E(object):... def f(x):... print x... f = staticmethod(f)...>>> print E.f(3)3>>> print E().f(3)3
使用非数据描述符协议,staticmethod()
的纯Python版本将如下所示:
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 E(object):... def f(klass, x):... return klass.__name__, x... f = classmethod(f)...>>> print E.f(3)('E', 3)>>> print E().f(3)('E', 3)
当函数只需要类引用而不需要其他数据的时候,这个行为就很有用了。类方法的一个用途是创建不同的类构造函数。在Python 2.3中,类方法dict.fromkeys()
从一个key
列表中创建一个新的词典。 纯Python的实现是:
class Dict(object): . . . def fromkeys(klass, iterable, value=None): "Emulate dict_fromkeys() in Objects/dictobject.c" d = klass() for key in iterable: d[key] = value return d fromkeys = classmethod(fromkeys)
现在可以像这样构建一个新的字典:
>>> Dict.fromkeys('abracadabra'){ 'a': None, 'r': None, 'b': None, 'c': None, 'd': None}
使用非数据描述符协议,classmethod()
的纯Python版本看起来像这样:
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
reference
转载地址:https://coordinate.blog.csdn.net/article/details/79517349 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!