Python 描述器解析

语法简析

一般来说,描述器(descriptor)是一个有”绑定行为”的对象属性(object attribute),它的属性访问被描述器协议方法重写。这些方法是 __get__()__set__()__delete__() 。如果一个对象定义了以上任意一个方法,它就是一个描述器。而描述器协议的具体形式如下:

descr.__get__(self, obj, type=None) --> value

descr.__set__(self, obj, value) --> None

descr.__delete__(self, obj) --> None

描述器本质上是一个类对象,该对象定义了描述器协议三种方法中至少一种。而这三种方法只有当类的实例出现在一个所有者类(owner class)之内时才有效,也就是说,描述器必须出现在所有者类或其父类的字典 __dict__ 里。这里提到了两个类,一是定义了描述器协议的描述器类,另一个是使用描述器的所有者类。

描述器往往以装饰器的方式被使用,导致二者常被混淆。描述器类和不带参数的装饰器类一样,都传入函数对象作为参数,并返回一个类实例,所不同的是,装饰器类返回 callable 的实例,描述器则返回描述器实例。

记住上面的话,下面我们举例说明。

@Property

Python 内置的 property 函数可以说是最著名的描述器之一,几乎所有讲述描述器的文章都会拿它做例子。

property 是用 C 实现的,不过这里有一份等价的 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 怎么用呢?看下面的例子:

class C(object):
    def __init__(self):
        self._x = None

    @Property
    def x(self):
        """I'm the 'x' property."""
        return self._x

    @x.setter
    def x(self, value):
        assert value > 0
        self._x = value

    @x.deleter
    def x(self):
        del self._x

我们结合源代码和用法来分析 Property

@Property 的用法就是一个装饰器。我们可以将其等价转化为:

x = Property(x)

函数 x 作为位置参数被赋给 Property.__init__()fget,得到新的 x 已经不是个函数而是个完整实现了 __get__() 方法的描述器实例了。

@x.setter 的用法略有不同。它实际上是利用上面定义的描述器实例 xsetter 方法,重新创建了新的实例。这时变量 x 再次被更新,指向了一个完整实现 __get__()__set__() 方法的新描述器。传入 setter 方法的函数名必须是 x,否则如果是 y,按照装饰器的性质,

y = x.setter(y)

新描述器就被 y 引用了,与需求不符。

Property 提供了像访问类“成员变量”一样访问 get、set 方法的能力。

In [123]: c = C()

In [124]: c.x = 1

In [125]: c.x
Out[125]: 1

In [126]: c.x = 0
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-126-b03deb420dcb> in <module>()
----> 1 c.x = 0

<ipython-input-50-95b8686aa4bd> in __set__(self, obj, value)
     20         if self.fset is None:
     21             raise AttributeError("can't set attribute")
---> 22         self.fset(obj, value)
     23
     24     def __delete__(self, obj):

<ipython-input-116-379a4e5fa639> in x(self, value)
     10     @x.setter
     11     def x(self, value):
---> 12         assert value > 0
     13         self._x = value
     14

AssertionError:

与一般的属性访问不同,c.x 访问的已经不是简单的属性,而是相当于 x.__get__(c),可以调用各种复杂方法对属性作检查、包装 。

那么,描述器是怎样被访问到的呢?

调用描述器

有两类描述器:如果同时定义了 __get__()__set__() 方法的描述器称为资料描述器(data descriptor),仅定义了 __get__() 的描述器称为非资料描述器(non-data descriptor)。非资料描述器常用于类的方法,如常见的 staticmethodclassmethod,都是其应用。

如前文所说,描述器常在所有者类或其实例中被调用。

对于实例对象,object.__getattribute__() 会把 c.x 转化为 type(c).__dict__['x'].__get__(c, type(c))。如果实例中有和描述器重名的属性 x 怎么办?资料和非资料描述器的区别在于,相对于实例字典的优先级不同。当描述器和实例字典中的某个属性重名,按访问优先级,资料描述器 > 同名实例字典中的属性 > 非资料描述器,优先级小的会被大的覆盖。上面的类 C 中,会优先访问资料描述器 x。下面将讲到,类的方法实际就是一个仅实现了 __get__() 的非资料描述器,所以如果实例 c 中同时定义了名为 foo 的方法和属性,那么 c.foo 访问的是属性而非方法。

对于类,type.__getattribute__()C.x 转化为 C.__dict__['x'].__get__(None, C)

有几点需要牢记的:

  1. 描述器被 __getattribute__() 方法调用
  2. 因而,重载 __getattribute__() 可能会妨碍描述器被自动调用
  3. __getattribute__() 仅存在于继承自 object 的新式类之中
  4. object.__getattribute__()type.__getattribute__()__get__() 的调用不一样
  5. 资料描述器总会覆盖实例字典,即资料描述器具有最高优先级
  6. 非资料描述器可能会被实例字典覆盖,即非资料描述器具有最低优先级

非资料描述器与类方法

Python 面向对象的特征建立在基于函数的环境之上。Python 用非资料描述器将二者无缝结合。

方法和普通函数唯一的区别就是,一般方法的第一个参数引用了当前实例,即通常命名为 self 的变量。

Python 中的函数,可以被认为是一个实现了 __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)

当函数作为属性被访问时,非资料描述器把函数变为一个方法,把实例调用 obj.f(*args) 转化成 f(obj, *args),把类调用 klass.f(*args) 转化为 f(*args)

更多绑定和转换参见下表。

转换 从对象调用 从类调用
函数 f(obj, *args) f(*args)
静态方法 f(*args) f(*args)
类方法 f(type(obj), *args) f(klass, *args)

静态方法是特殊的方法,可以无须实例化而在类中被直接调用,这时当然无法提供合法的 self。为此,需要实现 staticmethod 描述器,其 __get__() 返回的函数无需实例参数,其实也就是原样返回即可,可以用 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

类方法是另一种特殊的方法,无需当前实例 self, 但是需要当前类 klass (通常也写成 cls),纯 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

参考资料

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,511评论 18 139
  • 1.1. 摘要 定义描述器, 总结描述器协议,并展示描述器是怎么被调用的。展示一个自定义的描述器和包括函数,属性(...
    mutex73阅读 440评论 0 2
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,513评论 18 399
  • 本文翻译自python descriptor guide 摘要 本文定义了描述符,总结了其中的协议,并且介绍如何调...
    大蟒传奇阅读 1,162评论 0 5
  • 使用单独mongo命令载入相应mongo配置文件 mongod -f /etc/mongo.confmongod ...
    Zhang21阅读 926评论 0 1