Python描述器引导(翻译)

1.1. 摘要

定义描述器, 总结描述器协议,并展示描述器是怎么被调用的。展示一个自定义的描述器和包括函数,属性(property), 静态方法(static method), 类方法在内的几个Python内置描述器。通过给出一个纯Python的实现和示例应用来展示每个描述器是怎么工作的。
  学习描述器不仅让你接触到更多的工具,还可以让你更深入地了解Python,让你体会到Python设计的优雅之处。

1.2. 定义和介绍

一般来说,一个描述器是一个有“绑定行为”的对象属性(object attribute),它的访问控制被描述器协议方法重写。这些方法是 __get__(), __set__(), 和__delete__()。有这些方法的对象叫做描述器。
  默认对属性的访问控制是从对象的字典里面(__dict__)中获取(get), 设置(set)和删除(delete)它。举例来说, a.x的查找顺序是, a.__dict__['x'] , 然后type(a).__dict__['x'] , 然后找 type(a)的父类(不包括元类(metaclass)).如果查找到的值是一个描述器, Python就会调用描述器的方法来重写默认的控制行为。这个重写发生在这个查找环节的哪里取决于定义了哪个描述器方法。注意, 只有在新式类中时描述器才会起作用。(新式类是继承自 type或者 object的类)
  描述器是强大的,应用广泛的。描述器正是属性, 实例方法, 静态方法, 类方法和super的背后的实现机制。描述器在Python自身中广泛使用,以实现Python 2.2中引入的新式类。描述器简化了底层的C代码,并为Python的日常编程提供了一套灵活的新工具。

1.3. 描述器协议

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

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

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

这是所有描述器方法。一个对象具有其中任一个方法就会成为描述器,从而在被当作对象属性时重写默认的查找行为。
  如果一个对象同时定义了__get__()__set__(),它叫做资料描述器(data descriptor)。仅定义了__get__()的描述器叫非资料描述器(常用于方法,当然其他用途也是可以的)
  资料描述器和非资料描述器的区别在于:相对于实例的字典的优先级。如果实例字典中有与描述器同名的属性,如果描述器是资料描述器,优先使用资料描述器,如果是非资料描述器,优先使用字典中的属性。(译者注:这就是为何实例 a的方法和属性重名时,比如都叫foo,Python会在访问 a.foo的时候优先访问实例字典中的属性,因为实例函数的实现是个非资料描述器)
  要想制作一个只读的资料描述器,需要同时定义__set____get__,并在__set__中引发一个AttributeError异常。定义一个引发异常的__set__方法就足够让一个描述器成为资料描述器。

1.4. 描述器的调用

描述器可以直接这么调用: d.__get__(obj)
  然而更常见的情况是描述器在属性访问时被自动调用。举例来说, obj.d会在obj的字典中找d,如果d定义了__get__方法,那么d.__get__(obj)会依据下面的优先规则被调用。
  调用的细节取决于obj是一个类还是一个实例。另外,描述器只对于新式对象和新式类才起作用。继承于object的类叫做新式类。
  对于对象来讲,方法object.__getattribute__()b.x变成type(b).__dict__['x'].__get__(b, type(b))。具体实现是依据这样的优先顺序:资料描述器优先于实例变量,实例变量优先于非资料描述器,__getattr__()方法(如果对象中包含的话)具有最低的优先级。完整的C语言实现可以在 Objects/object.cPyObject_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__()只对新式类的实例可用
  • object.__getattribute__()type.__getattribute__()__get__()的调用不一样
  • 资料描述器总是比实例字典优先。
  • 非资料描述器可能被实例字典重写。(非资料描述器不如实例字典优先)

super()返回的对象同样有一个定制的__getattribute__()方法用来调用描述器。调用super(B, obj).m()时会先在obj.__class__.__mro__中查找与B紧邻的基类A,然后返回A.__dict__['m'].__get__(obj, A)。如果不是描述器,原样返回m。如果实例字典中找不到m,会回溯继续调用object.__getattribute__()查找。(译者注:即在__mro__中的下一个基类中查找)

注意:在Python 2.2中,如果m是一个描述器, super(B, obj).m()只会调用方法__get__()
。在Python 2.3中,非资料描述器(除非是个旧式类)也会被调用。super_getattro()的实现细节在: Objects/typeobject.c ,[del] 一个等价的Python实现在 Guido’s Tutorial [/del] (译者注:原文此句已删除,保留供大家参考)。
  以上展示了描述器的机理是在object, type, 和super__getattribute__()方法中实现的。由object派生出的类自动的继承这个机理,或者它们有个有类似机理的元类。同样,可以重写类的__getattribute__()方法来关闭这个类的描述器行为。

1.5. 描述器例子

下面的代码中定义了一个资料描述器,每次getset都会打印一条消息。重写__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.x
Retrieving var "x"
10
>>> m.x = 20
Updating var "x"
>>> m.x
Retrieving var "x"
20
>>> m.y
5

这个协议非常简单,并且提供了令人激动的可能。一些用途实在是太普遍以致于它们被打包成独立的函数。像属性(property), 方法(bound和unbound method), 静态方法和类方法都是基于描述器协议的。

1.6. 属性(properties)

调用property()是建立资料描述器的一种简洁方式,从而可以在访问属性时触发相应的方法调用。这个函数的原型:

property(fget=None, fset=None, fdel=None, doc=None) -> property attribute

下面展示了一个典型应用:定义一个托管属性(Managed Attribute) 。

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
        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。之后,对这个程序的改善要求在每次访问单元格时重新计算单元格的值。然而,程序员并不想影响那些客户端中直接访问属性的代码。那么解决方案是将属性访问包装在一个属性资料描述器中:

class Cell(object):
    . . .
    def getvalue(self, obj):
        "Recalculate cell before returning value"
        self.recalc()
        return obj._value
    value = property(getvalue)

1.7. 函数和方法

Python的面向对象特征是建立在基于函数的环境之上的。非资料描述器把两者无缝地连接起来。
  类的字典把方法当做函数存储。在定义类的时候,方法通常用关键字deflambda来声明。这和创建函数是一样的。唯一的不同之处是类方法的第一个参数用来表示对象实例。Python约定,这个参数通常是 self, 但也可以叫 this 或者其它任何名字。
  为了支持方法调用,函数包含一个__get__()方法以便在属性访问时绑定方法。这就是说所有的函数都是非资料描述器,它们返回绑定(bound)还是非绑定(unbound)的方法取决于他们是被实例调用还是被类调用。用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'] # 存储成一个function
<function f at 0x00C45070>
>>> D.f             # 从类来方法,返回unbound method
<unbound method D.f>
>>> d.f             # 从实例来访问,返回bound method
<bound method D.f of <__main__.D object at 0x00B18C90>>

从输出来看,绑定方法和非绑定方法是两个不同的类型。它们是在文件 Objects/classobject.c(http://svn.python.org/view/python/trunk/Objects/classobject.c?view=markup) 中用C实现的,PyMethod_Type是一个对象,但是根据im_self是否是 NULL (在C中等价于 None ) 而表现不同。
  同样,一个方法的表现依赖于im_self。如果设置了(意味着bound), 原来的函数(保存在im_func中)被调用,并且第一个参数设置成实例。如果unbound, 所有参数原封不动地传给原来的函数。函数instancemethod_call()的实际C语言实现只是比这个稍微复杂些(有一些类型检查)。

1.8. 静态方法和类方法

非资料描述器为将函数绑定成方法这种常见模式提供了一个简单的实现机制。
  简而言之,函数有个方法__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.
  既然staticmethod将函数原封不动的返回,那下面的代码看上去就很正常了:

>>> 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的引用作为第一个参数。不管 调用者是对象还是类,这个格式是一样的:

>>> 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:
    . . .
    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

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,637评论 18 139
  • 语法简析 一般来说,描述器(descriptor)是一个有”绑定行为”的对象属性(object attribute...
    ButteredCat阅读 4,068评论 1 4
  • 我只是有点难过啊,准备了那么长时间地课题,连蒋老师都说写得好,现在连校级课题也没有申请上…感觉自己白努力了…你看,...
    Darlinglingda阅读 194评论 0 0
  • 花落 花落奈何 伊人在 君已去 花落 花落凝香 曾走过 便是缘 唯过去不可更改 是比未来更踏实存在 足矣
    一只安静的兔子阅读 152评论 0 0
  • 我妈不喜欢《红楼梦》,她说那样惨淡破落的结局,令人心灰意冷,好些红红绿绿的女孩儿们,就这样死的死伤的伤,一副可怜相...
    江昭和阅读 477评论 2 9