python笔记整理-2.描述符和属性

这是一个非常重要的python特性,虽然出了自定义框架或者库一般不会用到,但是会帮助理解python的一些机制,了解一些错误是怎样发生的.

先来一个引入的例子

>>> class TestClass(object):pass
... 
>>> a = TestClass()
>>> a.x
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'TestClass' object has no attribute 'x'
>>> a.x=123
>>> a.x
123

2.1 介绍和定义描述符

描述符就是用来定义对象的属性行为的,与之相关联的是get(),set(),和delete()方法

默认的对一个属性的访问的顺序为

a.x 的查找顺序如下
a.dict['x'] -> type(a).dict['x'] -> 父类(不包含metaclass)

其间如果找到的是一个描述器(descriptor),python就会调用描述器来重写默认的行为,只有在新类(继承自object或者type的类)才会起作用

2.2 描述符的协议

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

只要具有上述任一一个方法的,就是描述器,这样当这个类被某个对象当成属性的时候,那它这个属性操作方法将会被相应的重写

定义了 __get____set__ 的叫做资料性描述符,只定义了 __get__ 的叫做非资料性描述符.

资料描述器和非资料描述器的区别在于:相对于实例的字典的优先级。如果实例字典中有与描述器同名的属性,如果描述器是资料描述器,优先使用资料描述器,如果是非资料描述器,优先使用字典中的属性。(译者注:这就是为何实例 a的方法和属性重名时,比如都叫 foo Python会在访问 a.foo的时候优先访问实例字典中的属性,因为实例函数的实现是个非资料描述器)

2.3 描述器的调用

描述器可以直接被调用d.__get__(obj)

然而没有人会这么用,通常我都是通过obj.d来获取obj里的d属性,如果这个d定义了__get__方法,那么就会调用这个__get__方法,遵从下面的优先级顺序.

详细的调用是,先看obj是一个类还是一个实例,不管怎样,描述符只会在新式类工作,继承于object的类.

对于对象而言,obj.__getattribute__()b.x转化成为 type(b).__dict__['x'].__get__(b, type(b)),
优先级顺序是,资料描述符-> 实例变量 -> 非资料描述符 -> __getattr__()(如果有定义)

对于类而言,type.__getattribute__()b.x转化为B.__dict__['x'].__get__(None, B),大概像下面这样:

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__()只在新式类中起作用.
  • obj.__getattribute__()type.__getattribute__()对于__get__()的调用不一样
  • 资料描述符将会覆盖对象的实例字典.
  • 非资料型描述符将会被实例的字典覆盖.

super返回的对象同样有一个定制的__getattribute__()方法来调用描述符,当调用super(B, obj).m将会在基类A里(距离B最近的)搜索obj.__class__.__mro__,然后返回A.__dict__['m'].__get__(obj, B),如果没有描述符,那么m就会被返回,如果不存在,就继续在下一个基类里去查找

描述符的原理就是在.object,type,super__getattribute__()方法中实现.由object派生出来的类自动继承,所以重写__getattribute__()可以关闭描述符机制.

2.4 描述符的例子

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, boundunbound,静态方法和类方法都是基于描述符的.

2.5 属性 properties

调用property()是建立资料描述符的一种简单的方式,从而可以在访问属性时候厨房相应的方法调用.

property(fget=None, fset=None, fdel=None, doc=None) -> property 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__)

例如,一个电子表格类提供了访问单元格的方式: Cell('b10').value 。 之后,对这个程序的改善要求在每次访问单元格时重新计算单元格的值。然而,程序员并不想影响那些客户端中直接访问属性的代码。那么解决方案是将属性访问包装在一个属性资料描述器中:

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

2.6 函数和方法

类的字典是把方法当做函数来存储,在定义类的时候,方法通常用关键字deflambda来声明,方法和函数的唯一区别是,方法的第一个参数是用表示对象的实例,python规定通常这个参数叫做self,也可以叫this.

为了实现方法的调用,函数包含一个__get__()方法一遍在属性访问时调用,所有的函数都是非资料型描述器,他们返回绑定还是非绑定的方法取决于他们是被实例调用还是被类调用,用paython来描述就是:

class Function(object):
    . . .
    def __get__(self, obj, objtype=None):
        "Simulate func_descr_get() in Objects/funcobject.c"
        return types.MethodType(self, obj, objtype)

下面是解释function的描述符是怎么工作的

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

2.7 静态方法和类方法

非资料描述符为将函数绑定成方法这种常见模式提供了一种简单的实现机制.

简而言之,函数有个方法 __get__() ,当函数被当作属性访问时,它就会把函数变成一个实例方法。非资料描述器把 obj.f(*args)的调用转换成 f(obj, *args)。 调用klass.f(*args) 就变成调用f(*args) 。

下面总结了绑定和它有用的2个变种

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变量的方法适合用静态方法.

利用非资料描述器, 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.中,dict.fromkeys()
 可以依据一个key列表来创建一个新的字典。等价的Python实现就是:

有点像java里的工厂方法模式

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(fromkey

现在,一个新的字典就可以这么创建:

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

参考: