python描述器(Descriptor)

描述器协议

描述器协议包括以下3个方法:

  • object.__get__(self, instance, owner)
    调用时得到类的属性(类属性访问控制)或者类的实例的属性(实例属性访问控制),owner是属性所在类,instance是属性所在类的实例或None(如果通过类进行访问的话)。该方法要么返回属性值,要么抛出AttributeError异常。
  • object.__set__(self, instance, value)
    调用时给实例的属性赋予新值
  • object.__delete__(self, instance)
    调用时删除实例的属性

描述器是什么

那什么是描述器呢?一般来讲,描述器就是有“绑定行为”的对象属性,该属性的访问控制被描述器协议(__get__(),__set__(),__delete__())重写。如果对象定义了任一个上述方法,那它就是描述器。

描述器分两种,一种是只定义了__get__()方法的描述器叫非资料描述器(non-data descriptor),另一种是定义了__get__()和__set__()的描述器叫资料描述器(data descriptor)。

来个小例子吧

class DataDescriptor(object):

    def __init__(self, val):
        self.val = val

    def __get__(self, instance, owner):
        print "get method of %s" % self.val
        return  self.val

    def __set__(self, instance, value):
        print "set method of %s" % self.val
        return self.val

class NonDataDescriptor(object):
    def __init__(self, val):
        self.val = val

    def __get__(self, instance, owner):
        print "get method of %s" % self.val
        return  self.val


class Context(object):
    dd = DataDescriptor("data descriptor")
    ndd = NonDataDescriptor("none data descriptor")

    def __init__(self, val):
        self.ndd = val

a = Context("haha~~")
a.dd
a.dd = "update"
a.dd

a.ndd
print(vars(Context))
print(vars(a))
print(a.ndd)
print(a.__dict__['ndd'])
print(type(a).__dict__['ndd'].__get__(a,Context))
#输出
get method of data descriptor
set method of data descriptor
get method of data descriptor
{'__module__': '__main__', 'dd': <__main__.DataDescriptor object at 0x10445a610>, 'ndd': <__main__.NonDataDescriptor object at 0x10445a650>, '__dict__': <attribute '__dict__' of 'Context' objects>, '__weakref__': <attribute '__weakref__' of 'Context' objects>, '__doc__': None, '__init__': <function __init__ at 0x104457938>}
{'ndd': 'haha~~'}
haha~~
haha~~
get method of none data descriptor
none data descriptor

描述器的调用

为说明描述器的调用,先看了解一下属性的查找过程,属性查找的过程也就是调用的过程。

实例属性的查找过程

对于a = A(), a.attr会先调用实例的__getattribute__, 对描述器方法__get__的调用就发生在__getattribute__的内部,如果该方法抛出AttributeError异常,而且类A有定义__getattr__方法,这时__getattr__会被调用。如果将__getattribute__内部查找顺序考虑在内,则属性的默认查找顺序是这样的:

  • 如果attr在A或其基类的__dict__中, 且attr是资料描述器, 那么调用其__get__方法, 否则
  • 如果attr出现在a的__dict__中, 那么直接返回a.__dict__['attr'], 否则
  • 如果attr出现在A或其基类的__dict__
    • 如果attr是非资料描述器,那么调用其__get__方法, 否则
    • 返回__dict__['attr']
  • 如果A有__getattr__方法,调用__getattr__方法,否则
  • 抛出AttributeError

类属性的查找过程

考虑到python中类也是对象,类是元类(metaclass)的实例,这样对应之后,类属性的查过程与实例属性的查找过程就基本相同了,除了第二步。由于类可能有父类,所以这里第二步就变成了:
只要A.__dict__['attr']是一个descriptor,都调用其__get__方法,否则返回A.__dict__['attr']
这一步用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__可以修改默认的描述器调用,甚至关闭描述器的调用。另外,描述器只在新式类(继承自object,python 2.2引入)可用。

应用

方法(method)

python面向对象的特征是建立在基于函数的环境之上的,非资料描述器就是两者之间的桥梁。函数中有__get__()方法以方便在属性访问时绑定方法,也就是说所有的函数都是非资料描述器。类的字典中将方法存成函数,类定义的时候,与函数的声明一样,方法使用def或lambda来声明。

在python中,类似a.b()这样的方法调用是分两步的:获取属性a.b和调用。第一步是个参数绑定的过程,返回一个可调用的bound method对象,如果不能进行进行参数绑定则返回unbound method对象,不可调用。

class Method(object):

    name = "class name"

    def __init__(self,name):
        self.name = name

    def instance_method(self):
        return self.name

    @classmethod
    def class_method(cls):
        return cls.name

    @staticmethod
    def static_method(x,y):
        return x + y
  • 实例方法(instancemethod)
print(Method.__dict__['instance_method'])


a = Method("haha~")
print(a.instance_method)
print(type(a).__dict__['instance_method'].__get__(a,type(a)))
print(Method.instance_method)
print(Method.__dict__['instance_method'].__get__(None,Method))
print(a.instance_method())
print(Method.instance_method(a))

#输出
<function instance_method at 0x100a6a2a8>
<bound method Method.instance_method of <__main__.Method object at 0x100b5f590>>
<bound method Method.instance_method of <__main__.Method object at 0x100b5f590>>
<unbound method Method.instance_method>
<unbound method Method.instance_method>
haha~
haha~

实例方法调用时,需要有一个实例才能绑定,对于a.instance_method()这个实例就是函数定义时的self,对于Method.instance_method(a)这个实例就很明显了。

  • 类方法
print(vars(a))
print(vars(Method))
print(a.class_method)
print(type(a).__dict__['class_method'].__get__(a,type(a)))
print(type(a).__dict__['class_method'].__get__(None,type(a)))
print(type(a).__dict__['class_method'].__get__(a,type(a))())
print(type(a).__dict__['class_method'].__get__(None,type(a))())

print(Method.class_method)
print(Method.__dict__['class_method'].__get__(None,Method))
print(Method.__dict__['class_method'].__get__(None,Method)())

#输出
{'name': 'haha~'}
{'__dict__': <attribute '__dict__' of 'Method' objects>, '__module__': '__main__', 'static_method': <staticmethod object at 0x10d864e88>, 'name': 'class name', 'instance_method': <function instance_method at 0x10d7832a8>, '__weakref__': <attribute '__weakref__' of 'Method' objects>, 'class_method': <classmethod object at 0x10d864e50>, '__doc__': None, '__init__': <function __init__ at 0x10d7830c8>}
<bound method type.class_method of <class '__main__.Method'>>
<bound method type.class_method of <class '__main__.Method'>>
<bound method type.class_method of <class '__main__.Method'>>
class name
class name
<bound method type.class_method of <class '__main__.Method'>>
<bound method type.class_method of <class '__main__.Method'>>
class name

类方法绑定参数是类,实例并没有用。通过实例调用时,实例a中并没有class_method属性,按属性的查找过程,会到类里查找,并通过非资料描述器的__get__方法调用,通过Method调用时,参考前面的类属性查找过程。

  • 静态方法
print(a.static_method)
print(type(a).__dict__['static_method'])
print(type(a).__dict__['static_method'].__get__(a,type(a)))
print(type(a).__dict__['static_method'].__get__(None,type(a)))

#输出
<function static_method at 0x103645848>
<staticmethod object at 0x103634e18>
<function static_method at 0x103645848>
<function static_method at 0x103645848>

静态方法是一类特殊的方法,由定义可知,静态方法并不需要绑定到实例或类上,它只是一个普通的函数,调用时不再需要返回bound method对象。

待续

参考:
https://docs.python.org/3/reference/datamodel.html
https://www.cnblogs.com/xybaby/p/6270551.html
https://www.jianshu.com/p/250f0d305c35

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,591评论 18 139
  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,857评论 6 13
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,565评论 18 399
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,678评论 0 9
  • 认识我的人都知道我是一个典型的双子座性格。双子座的人善变,也因此被贴上了各种各样的标签。 不过今天我突然领悟这其实...
    樊语朱心阅读 517评论 0 1