看书 Python 源码分析笔记 (九) 类机制二

今天继续学习第十二章 类体系(下)

用户自定义 class

本节学习用户自定义 type (class). 下面的示例 python 类包含类定义, 构造函数, 实例化等多个要点:

class A:  # 类缺省派生自 object
  name = 'Python'          # 属性
  def __init__(self):      # 这是构造函数
    print 'A::__init__'
  
  def f(self):             # 一般成员函数
    print 'A::f'
  
a = A()   # 实例化
a.f()     # 调用成员函数.

(编译后形成多个 PyCodeObject, 以及其关系略)

class 的动态元信息

这里 class 的元信息指关于 class 自身的信息, 如 class 的名字, 含有什么属性,方法, 空间大小等.
如这个 python 类 A, 含一个名字 f, 其对应一个方法(method). 根据这些元信息, 才能创建出此类的 instance.
可对应到 java/c# 中的 Class (反射)信息, python 也提供这种元信息, 并具备高度灵活的动态性.

Python 字节码相关创建类的指令有 BUILD_CLASS,MAKE_FUNCTION.
估计是使用一组指令序列创建出一个 class 实体, 然后向该 class 添加 name, __init__, f 名字->值的对.
这些值对被存入 frame 的 locals 名字空间, 也即存放的是 class A 的(动态)元信息.

此后建立好的 locals 名字空间(即包含 A 的元信息的 dict) 传递给指令 BUILD_CLASS, 对应内部函数
build_class(methos, bases, class_name):

// ceval.c
PyObject *build_class(dict methods, tuple bases, string name) {
  ...
  result = PyObject_CallFunctionObjArgs(metaclass, name, bases, methods)
  ...
}

这里 methods 参数即是前面 locals, 含新类的所有(程序员给出的)元信息, 包括属性和方法.
该函数前面计算一个 metaclass 参数, 与 classic class(旧式类) 和 new style class 有关, 历史遗留问题...
如果未指定 metaclass, 则选用第一基类(缺省是object) 的 type 作为 metaclass.
也即 <type 'type'> .

图12-21 显示多个 class 对象的静态元信息, 动态元信息的关系.

传递到函数 PyObject_CallFunctionObjArgs() 的第一个参数是 <type 'type'>, 即内部的 PyType_Type
对象, 这个对象是 callable 的, 对它进行 call 产生的效果就是构造出一个 class 对象.

// abstract.c
PyObject *PyObject_Call(PyObject *func, PyObject *args ...) {
  // 这里 args 可认为是要新建的 class 的 name,bases,methods 打包...
  XXX_FuncType *call = func->ob_type->tp_call;
  PyObject *result = (*call)(func as this, args); 
  // 这个返回结果应该就是新建的 class.
}

也即对 Type metaclass 的调用, 就会产生一个新的 class 对象. 道理同对 class 的调用就会产生一个 instance .
这些繁杂的创建一个 Type metaclass 的 instance 的事务发生在:

// typeobject.c
PyObject *type_new(PyTypeObject *metatype, PyObject *args ... ) {
  // 这里 metatype 即是 PyType_Type, args 含类名,基类,方法集合.
  // 解析变量...
  var name = args[name],
      bases = args[bases],
      dict = args[dict];   // 伪代码.
  
  // 申请内存, 为放置在 heap 中的 type 对象: heap-type.
  PyHeapTypeObject *type = metatype->tp_alloc(...);
  
  // 初始化 type 的很多域(略).
  type->xxx = xxx ......

  // 要点: 调用 PyType_Ready() 就绪此 type(class) 对象.
  PyType_Ready(type);
  return type;  // 返回新建的 class 对象.
}

这下也就明白了, PyHeapTypeObject 就是专门为用户自定义的 class 对象准备的.

同时我们对 Python 中 "可调用(callable)" 概念也有一定的感性认识了. 在 python 中, 创建一个类的实例
直接用 a = A() 即将这个类作为 callable 调用即可, 与其它语言的 a = new A() 就稍稍有点差异. (和 js 像).

从 class 对象到 instance 对象

终于, 前面准备 class 的曲折工作完成了, 后面就是用这个 class 来创建 instance 了. 虚拟机指令示例如下:

a = A()  # 创建 class A 的新实例(instance)
   LOAD_NAME  'A'  # class 的名字, 在 globals 或 locals 名字空间中.
   CALL_FUNCTION   # 当做 callable 调用, 创建出一个 instance.
   STORE_NAME 'a'  # 新对象绑定到名字 'a'

也即验证了 "调用 class 对象将创建 instance 对象". (这里我们可以认为 a = new A() 是 a = A() 的语法糖).
所谓调用, 就是内部对 type->tp_call 函数的调用, 这里最终调用到 A.tp_new() 函数, 从而创建出 instance.

对于 type 是 tp_new 函数, 对于一般 class 是 object_new 函数. 后者核心工作是分配一个 PyXXXObject
所需空间(大约24字节), 然后初始化这个对象的 type=class A, 然后这个对象就是 A 的 instance 了.
对象创建之后, 会调用 tp_init, 如果对象重写 __init__ 方法, 则会调用到以实现构造语义.
(这里搜索 __init__ 方法涉及到 mro 搜索)

小结, 从 class 对象创建 instance 对象的两步骤:
   1. instance = class.__new__(class, args, kwds)
   2. class.__init__(instance, args, kwds)

参数 args 通常含创建参数; kwds 是键参数, 通常为 NULL.

从 metaclass 对象创建 class 对象的过程, 与从 class 对象创建 instance 的过程相同.
这样如前面所提及, class 对象具有二相性. (被别人创建, 也创建别人, 以同样的方式)

 

访问 instance 对象中的属性

在 python 中, 可访问对象的属性如 x.y, x.f() (方法可认为是广义的属性). 访问属性,方法的字节码为:

a.f()
   LOAD_NAME  'a'   # 访问对象 a
   LOAD_ATTR  'f'   # 访问对象 a 的属性 f.
   CALL_FUNCTION    # 将 f 当做函数调用, 即完成 a.f() 语义.

这里 LOAD_ATTR 即读取属性的指令, 实现为调用函数 PyObject_GetAttr(obj, attr_name).

// object.c
PyObject *PyObject_GetAttr(PyObject *v, PyObject *name) {
  type = v->tp_type;  // 对象的类型.
  if (type->tp_getattro)  // 如果提供有这个访问函数, 则用它查找.
    return (*type->tp_getattro)(v, name);

  if (type->tp_getattr)  // 如果提供 getattr 访问函数, 则用它...
    return (*type->tp_getattr)(v, name);
  
  // 未提供访问方法, 则抛出异常.
  throw new PyExc_AttributeError;
}

这里 python 的首选属性访问函数为 tp_getattro, 次之的 tp_getattr 已不再推荐使用(细微区别在参数).

在创建类 A 的时候, 会从基类 object 继承 tp_getattro -- PyObject_GenericGetAttr, 所以会调用它.
而这里有一套复杂的确定访问属性的算法. 如访问 a.f 为例:

# 首先寻找 'f' 的 descriptor
# 注意: hasattr 会在 <class A> 的 mro 列表中寻找符号 'f'
if hasattr(A, 'f'):
  descriptor = A.f

type = descriptor.__class__
if hasattr(type, '__get__') and (hasattr(type, '__set__') or 'f' not in a.__dict__):
  return type.__get__(descriptor, a, A)

# 通过 descriptor 访问失败, 在 instance 对象自身的 __dict__ 中寻找属性
if 'f' in a.__dict__:
  return a.__dict__['f']

# 还找不到 ...
if descriptor:
  return descriptor.__get__(descriptor, a, A)

 

这里包含的重要概念是 descriptor.

在上面的属性访问算法中, 涉及到了对象 a 的 __dict__ 属性. 创建类 A 时会设置一个 tp_dictoffset 域,
根据名字看是指 instance 对象中 __dict__ 偏移位置. 图12-24 描述此结构. 在 instance 中有一个字段
放置 dict 对象, 偏移由 type->tp_dictoffset 给出. 找到该 dict 之后, 从该 dict 中查找出名为 name 的
属性, 作为值返回. dict 本身在对称的 setter 中第一次发现为 null 时被创建.

回顾前面的 PyType_Ready() 中, 填充的 tp_dict 即是名字->descriptor.
几个有重大影响的操作 __get__, __set__, __delete__.

descriptor 有数种: PyWrapperDescrObject, PyMethodDescrObject, data-descriptor

在 Python 虚拟机访问 instance 对象的属性时, descriptor 影响对属性的选择. 首先会在 instance.__dict__
中寻找属性, 也会在 instance.__class__.mro 中寻找属性. 前者称为 instance 属性, 后者称为 class 属性.

书上根据效果总结两条规则(...看着有点乱...):
1. 按照 instance 属性, class 属性的顺序选择属性, 即前者优先于后者.
2. 如果在 class 属性中发现同名的 data descriptor, 则该 descriptor 优先于 instance 属性.
  (这里需要看介绍 python 细节的书, 以及看源码)

当最终获得的属性是一个 descriptor 时, 不是返回 descriptor 本身, 而是调用其 descriptor.__get__
后将结果返回.

(我觉得这里应先看看 python 语言的书, 并关注下属性访问细节, 然后再看源码会更好)

另有细节区别于 class.tp_dict 中的 descriptor 和 instance.tp_dict 的. 需要跟踪源码了解.

这里 descriptor 对属性访问的影响包含两方面: 1. 访问顺序; 2.访问结果.
后者是类的成员函数调用的关键.

当访问 a.f() 的时候, 先获取 a.f 的属性并压栈, 并使用 CALL_FUNCTION 指令调用, 那么 self 参数在哪里?
引用爱因斯坦: 为了相对论, 必须抛弃绝对时空的观念.

这里 a.f 返回的不是 PyFunctionObject, 由于 A.f 是一个 descriptor, 从而调用 A.f.__get__:

// funcobject.c
// 绑定函数到一个对象.
PyObject *func_descr_get(PyObject *func, PyObject *obj, PyObject *type) {
  // ... 其它检查略 ...
  return PyMethod_New(func, obj, type);
}

// classobject.c
PyObject *PyMethod_New(PyObject *func, PyObject *self, PyObject *class) {
  ... 检查等代码略 ...
  var m = new PyMethodObject();
  m->im_func = func;
  m->im_self = self;      // 这里重点是 method 中绑定了 self 对象.
  m->im_class = class;    // 还绑定了所属类?
  return m; 
}

(我觉得概念上可理解 method 即是 js 中的 f.bind(this))

这样 a.f 首先得到的是一个 PyMethodObject. 在 python 中, 将 PyFunctionObject 对象和一个 instance
对象通过 PyMethodObject 结合在一起的过程: 称为成员函数的绑定.

这样, 我们就知道了对 a.f() 的调用实现细节步骤:
1. 找到 a.f 属性, 是一个 function-obect;
2. 绑定 function-objet + a作为self 形成一个 method-object
3. 调用 method-object, 等价于调用 function-object(a as self).

第3步在 call_function() 函数中有体现:

// ceval.c
PyObject *call_function(...) {
  ... 前面略 ...
  else if (method is PyMethodObject) {
    PyObject *self = method->get_self();      // 取出绑定的 self 参数.
    PyObject *func = method->get_function();  // 取出实际函数对象.
    // 调用该 func, 带上 self 参数(语义上) ...
    do_call(func, self, 其它参数...)
  }
  else ...
}

 

书上总结 Python 运行模型, 高度简化为两条规则:
1. 在某个名字空间寻找符号对应的对象.
2. 对该对象进行某些操作.

 

Bound Method 和 Unbound Method

Python 中引用类的函数有两种形式, 一是 Bound method 如用 a.f 形式引用的; 二是 Unbound Method,
如用 A.f 形式引用的. 内部区别即是 PyMethodObject 和 PyFunctionObject 的区别.

由于每次对成员函数的调用, 都会创建新的 method-object 对象, 这样开销比较大. 实验: 见书.

 

千变万化的 descriptor

前述调用 instance 成员函数时, 最关键的动作即是从 func->method 的转变, 此转变由 descriptor 完成.

在 python 内部, 存在着各种各样的 descriptor. 下面简介如何用 descriptor 实现 static method.

通过 python 内置的 staticmethod 函数(实际是一个 type object), 将一个函数转变为属性...
(细节暂时略, 需要先看 python 书了解下 staticmethod 的语义和应用)

(本章结束)

由于类机制是十分复杂, 且极端重要的部分, 为了(差不多)学懂, 也得多花点时间看书,实验,翻看源码, 不能着急.

你可能感兴趣的:(看书 Python 源码分析笔记 (九) 类机制二)