python描述符使用指南
发布日期:2021-06-29 16:00:28 浏览次数:2 分类:技术文章

本文共 8041 字,大约阅读时间需要 26 分钟。

#简介

通常,描述符是具有 ” 绑定行为 “ 的对象属性,其属性访问被描述符协议中的方法重写。这些方法是__get__()__set__()__delete__()。如果对象中定义了这些方法中的任何一种,则称它为描述符。

属性访问的默认行为是从对象的字典中获取,设置或删除属性。例如,a.x有一个以a.__dict__['x']开始的查找链,接着是type(a).__dict__['x'],然后是除了元类之外的type(a)的基类。如果查找的值是定义其中一个描述符方法的对象,则Python可以调用描述符方法重写默认行为。在优先链中发生这种情况取决于定义了哪些描述符方法。请注意,描述符仅针对new style对象或类进行调用(如果类继承自objecttype,则该类为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.dobj的字典中查找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()中。

上面的细节表明描述符的机制嵌入在objecttypesuper()__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的面向对象功能建立在基于功能的环境之上。使用非数据描述符可以将两者无缝连接。

类字典将方法作为函数存储。在类定义中,方法使用deflambda编写,这是创建函数的常用工具。与常规函数的唯一区别是第一个参数是为对象实例保留的。 按照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.fC.f相当于object.__ getattribute__(c,"f")object.__getattribute__(C,"f")。因此,函数可以从对象或类中以相同的方式访问。

不引用self的方法适合成为静态方法。

例如,一个统计软件包可能包含实验数据的容器类。该类提供了用于计算数据的平均值,中值和其他描述性统计量。但是,可能有一些有用的功能在概念上是相关的,但不依赖于数据。例如,erf(x)是一个经常用到的例子,它出现在统计工作中,但不依赖于特定的数据集。它可以从对象或类中调用:s.erf(1.5)-> .9332Sample.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 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:深入理解python super
下一篇:super(type, self)与super(type1, type2)的区别

发表评论

最新留言

路过按个爪印,很不错,赞一个!
[***.219.124.196]2024年05月02日 07时12分01秒

关于作者

    喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
-- 愿君每日到此一游!

推荐文章

【Python爬虫实战】知乎热榜数据采集,上班工作摸鱼两不误,知乎热门信息一网打尽 2019-04-29
自从我学会了数据挖掘Matplotlib、Numpy、Pandas、Ta-Lib等一系列库,我把领导开除了 2019-04-29
Python抓取哔哩哔哩up主信息:只要爬虫学的好,牢饭吃的早 2019-04-29
有个码龄5年的程序员跟我说:“他连wifi从来不用密码” 2019-04-29
领导让我整理上个季度的销售额,幸好我会Python数据分析,你猜我几点下班 2019-04-29
【Python爬虫实战】为何如此痴迷Python?还不是因为爱看小姐姐图 2019-04-29
2021年6月全国程序员薪资出炉,大佬您上榜了吗? 2019-04-29
零基础自学Python,你也可以实现经济独立! 2019-04-29
ElasticSearch与Mysql对比(ElasticSearch常用方法大全,持续更新) 2019-04-29
数字化转型的主干道上,华为云以“三大关键”成企业智能化推手 2019-04-29
数字化为何不走“捷”“径”? 2019-04-29
和总裁、专家交朋友,华为云助推政企智能化升级又做到前面去了 2019-04-29
BCOP章鱼船长,6月22日晚上8点上线薄饼 2019-04-29
为战疫助力,半导体功不可没 2019-04-29
了解这些操作,Python中99%的文件操作都将变得游刃有余! 2019-04-29
知道如何操作还不够!深入了解4大热门机器学习算法 2019-04-29
只有经历过,才能深刻理解的9个编程道理 2019-04-29
发现超能力:这些数据科学技能助你更高效专业 2019-04-29
AI当道,人工智能将如何改变金融业? 2019-04-29
消除性别成见,技术领域需要更多“乘风破浪的姐姐” 2019-04-29