[TOC]

1 Python 的 @property/method 是由描述符实现的

Transformation 描述符类别 类别不同导致结果
@property 覆盖型(data descriptor) 实例不能覆盖 @property
method/staticmethod/classmethod 非覆盖型(non-data descriptor) 不同的实例可以自定义自己的 method

2 定义

  1. 白话版: 用来控制对象 设置属性, 获取属性, 删除属性的一个 object
  2. 实现__get__(), __set__(), and __delete__()
  3. Python 使用点.读取对象的属性, 默认读取对象属性是从, 对象的属性字典中获取
    • 属性调用链: 比如a.x查找的顺序是a.__dict__['x'] > type(a).__dict__['x'] > … 访问所有的基类, 包括 metaclass
    • 如果查找的值是通过描述符实现的, 描述符方法会改变这个调用链
  4. Raymond Hettinger 原文:

In general, a descriptor is an object attribute with “binding behavior”, one whose attribute access has been overridden by methods in the descriptor protocol. Those methods are get(), set(), and delete(). If any of those methods are defined for an object, it is said to be a descriptor.

3 协议

1
2
3
descr.__get__(self, obj, type=None) --> value
descr.__set__(self, obj, value) --> None
descr.__delete__(self, obj) --> None
  • data descriptor: 覆盖型描述符(数据描述符), 同时定义__get____set__方法
  • non-data descriptor: 非覆盖型描述符(非数据描述符), 只定义了__get__方法
  • 其两者区别在于, 当读写属性时, 优先级是否比实例字典属性高
  • 即是否覆盖实例字典属性(object.dict)

在 <流畅的 Python> 里, data descriptor称为overridinng descriptor overridinng descriptor, 翻译为覆盖/非覆盖一眼便知其特点, 推荐使用这个词

4 调用

类别 a.x =>
直接调用 x.__get__(a)
实例绑定 object.__getattribute__() => type(a).__dict__['x'].__get__(a, type(a))
类绑定 type.__getattribute__() => A.__dict__['x'].__get__(None, A)
super(B, obj).m() 先从 __class__.__mro__ 里找到 A => A.__dict__['m'].__get__(obj, obj.__class__)

a 是 类 A 的实例

Direct Call The simplest and least common call is when user code directly invokes a descriptor method: x.get(a).

Instance Binding If binding to a new-style object instance, a.x is transformed into the call: type(a).dict[‘x’].get(a, type(a)). Class Binding If binding to a new-style class, A.x is transformed into the call: A.dict[‘x’].get(None, A). Super Binding If a is an instance of super, then the binding super(B, obj).m() searches obj.class.mro for the base class A immediately preceding B and then invokes the descriptor with the call: A.dict[‘m’].get(obj, obj.class).

  • 可以通过直接调用方法名来调用描述符。例如:d.__get__(obj)

  • 根据调用主体的类型是对象(object) 或是 类(class), 调用过程的区别是:

    • 如果是 object, 调用机制位于object.__getattribute__(), 把b.x 转换成了type(b).__dict__['x'].__get__(b, type(b)), 完整的实现细节位于 Objects/object.c 中的 PyObject_GenericGetAttr()

    • 如果是 class, 调用机制位于type.__getattribute__(), 把B.x转换成B.__dict__['x'].__get__(None, B), 其实现细节 Python 版:

      1
      2
      3
      4
      5
      6
      
      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.__getattribute__()type.__getattribute__()__get__()的调用不一样
    • 覆盖型描述符, 总是覆盖实例字典属性(instance dictionaries)
    • 非覆盖型描述符, 可能会被实例字典属性覆盖

除了这一点: 类可以通过重写__getattribute__()来控制描述符的触发, 其他的地方在<流畅的 Python> 里写的更清楚

5 栗子

  1. RevealAccess, 监控属性访问

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    
     class RevealAccess(object):
         """
         类名意为: 显示访问
         可以对 set value & get value 进行监控并打印日志的描述符
         """
        
         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
        
     In [6]: class MyClass(object):
        ...:     x = RevealAccess(10, 'var "x"')
        ...:     y = 5
        ...:
        
     In [7]: m = MyClass()
        
     In [8]: m.x
     Retrieving var "x"
     Out[8]: 10
        
     In [9]: m.x = 20
     Updating var "x"
        
     In [10]: m.x
     Retrieving var "x"
     Out[10]: 20
        
     In [11]: m.y
     Out[11]: 5
    

6 @property

  1. 一句话: @property 是覆盖型描述符

  2. property()

    • 签名:

      • property(fget=None, fset=None, fdel=None, doc=None) -> property attribute
  • 文档示例 property 的用法:

      ```python
      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.")
      ```
    
  1. 描述符实现 property() 的源码 Python 版:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    
     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__)
    
  2. 回想一下, 我们是怎么用 @property 的:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
     class User(object):
         """使用 @property 控制用户设置的年龄不为负"""
         def __init__(self, age):
             self.age = age
        
         @property
         def age(self):
             return self._age
         # @property 语法糖相当于:
         # def age(self):
         #     return self._age
         # age = Property(age) # fget=age
        
         @age.setter
         def age(self, value):
             if value < 0 :
                 raise ValueError('age must be positive!')
             self._age = value
         # 相当于
         # age = age.setter(value) # fset = value
         # 即 age = type(age)(self.fget, fset, self.fdel, self.__doc__)
         # 即 age = Property(fget=self.fget, fset=value, self.fdel, self.__doc__)
         # fget 和 fset not None, Property 描述符`__set__`和`__get__`都实现了, 是覆盖型描述符,
    
    • @property 使用了装饰器语法糖
    • @age.setter返回了新的 Property 实例, 实现了描述符的__set__方法
    • 旧式类只支持__get__, 所以 @property 不能使用 @property.setter
  3. property() 的作用是, 动态的修改对属性的控制

    • 应用举例: 通过Cell('b10').value访问一个表格的数据, 现在有一个新需求, 要求要重新计算此单元格的值, 而且不修改之前代码, 解决方法就是 property

      1
      2
      3
      4
      5
      6
      7
      
      class Cell(object):
          ...
          def getvalue(self):
              "Recalculate the cell before returning value"
              self.recalc()
              return self._value
          value = property(getvalue)
      

7 functions & methods

  1. 一句话总结: 绑定方法对象(bound method)是非覆盖型描述符

  2. Python 面向对象的特性是建立在函数这一概念之上的, Python 把函数(function)与面向对象(OO, 就是类啊子类啊什么的)两者结合到一起的就是非覆盖型描述符

  3. 类外函数称为(Function), 类内称为(Method)

    • 类字典存储着 method 作为 functions, 在 class 中, method 使用deflambda
    • method 与 function 的区别
      • method 接受的第一个参数是 self
    • 实例引用称为 self
  4. 所有用户定义的函数都有__get__方法, 所以, 当这些函数被一个对象调用时(依附到类上), 都是非覆盖型描述符, 且返回一个bound method(绑定方法对象)

  5. Function 源码 Python 版, 函数(Function)也用到了描述符, 实现了__get__方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
     class Function(object):
         . . .
         def __get__(self, obj, objtype=None):
             "Simulate func_descr_get() in Objects/funcobject.c"
             if obj is None:
                 return self
             return types.MethodType(self, obj)
             # 如果没有 obj, 直接返回
             # 有 obj, 返回 method type 类型
    
  6. ipython:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    
     In [36]: class D(object):
         ...:     def f(self, x):
         ...:         return x
         ...:
     In [37]: d = D()
        
     # 通过类字典属性访问函数, 不会触发描述符`__get__`(非覆盖型描述符)
     # 只会返回实际函数对象
     In [38]: D.__dict__['f']
     Out[38]: <function __main__.D.f>
        
     # 点访问, 触发描述符`__get__()`, `obj` is None, 同样返回实际函数对象
     In [39]: D.f
     Out[39]: <function __main__.D.f>
        
     # 函数对象有`__qualname__`属性支持自省
     In [40]: D.f.__qualname__
     Out[40]: 'D.f'
        
     # 实例调用, 触发描述符`__get__`, 返回一个实例 d 的绑定方法对象(bound method), (function wrapped in a bound method object)
     In [41]: d.f
     Out[41]: <bound method D.f of <__main__.D object at 0x104568240>>
        
     # 其实 bound method 把基础函数(underlying function), 绑定的实例和绑定实例的类,都存了起来
     In [42]: d.f.__func__
     Out[42]: <function __main__.D.f>
        
     In [44]: d.f.__self__
     Out[44]: <__main__.D at 0x104568240>
        
     In [45]: d.f.__class__
     Out[45]: method
        
     In [54]: d
     Out[54]: <__main__.D at 0x104568240>
        
     In [55]: D
     Out[55]: __main__.D
    
  7. 这一节在 <流畅的 Python> 中是 20.3 方法是描述符

8 staticmethod & classmethod

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)
  1. 非覆盖型描述符, 把obj.f(*args)转换成f(obj, *args), 调用klass.f(*args)最终调用的是f(*args)

    • c.f => object.getattribute(c, “f”)
    • C.f => object.getattribute(C, “f”)
    • As a result, the function becomes identically accessible from either an object or a class.
  2. 静态方法特点是谁调用都返回

    • 用法举例: 统计学中的一些公式啊, 比如说 erf(x) 误差函数, 直接拿来用, 一般不会变动的函数, 无论是实例还是类调用都不改变其逻辑

    • staticmethod 源码 Python 版:

      1
      2
      3
      4
      5
      6
      7
      8
      
      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
      
  3. classmethod, 无论实例调用还是类调用, 都会在调用前将类的引用作为函数的第一个参数

    • 类方法的一种作用是创建多种类构造器

    • classmethod 源码 Python 版:

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      
      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
      

9 super()(TODO)

  1. super() 的概念:

    • super() 不是父类,而是继承顺序(MRO)里的下一个类

    • help(super)

      1
      2
      3
      
      |  super() -> same as super(__class__, <first argument>)
      |  super(type) -> unbound super object
      |  super(type, obj) -> bound super object; requires isinstance(obj, type)
      

    | super(type, type2) -> bound super object; requires issubclass(type2, type) ```

  2. super() 原理:

  • 覆盖了 __getattribute__方法
  1. 如何使用

    1
    
     super(cls, instance-or-subclass).method(*args, **kw)
    

    可以说对应这个:

    1
    
     right-method-in-the-MRO-applied-to(instance-or-subclass, *args, **kw)
    

参考:

流畅的Python

Descriptor HowTo Guide

How does the @property decorator work?

Descriptor Protocol

Things to Know About Python Super