0%

内部结构#

float 实例对象在 Include/floatobject.h 中定义如下:

1
2
3
4
typedef struct {
PyObject_HEAD //定长对象共用的头部
double ob_fval; //额外字段,存储对象所承载的浮点值
} PyFloatObject;

下面是浮点实例对象内部结构图:

float 类型对象#

与实例对象不同, float 类型对象 全局唯一 ,因此可以作为 全局变量 定义。 在 C 文件 Objects/floatobject.c 中,我们找到了代表 float 类型对象的全局变量 PyFloat_Type

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
39
40
PyTypeObject PyFloat_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"float", /*tp_name 字段保存类型名称,常量 float*/
sizeof(PyFloatObject),
0,
(destructor)float_dealloc, /* tp_dealloc 对象销毁相关*/
0, /* tp_print */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_reserved */
(reprfunc)float_repr, /* tp_repr 生成语法字符串表示形式的函数*/
&float_as_number, /* tp_as_number 数值操作集*/
0, /* tp_as_sequence */
0, /* tp_as_mapping */
(hashfunc)float_hash, /* tp_hash 哈希值生成函数*/
0, /* tp_call */
(reprfunc)float_repr, /* tp_str 生成普通字符串表示形式的函数*/
PyObject_GenericGetAttr, /* tp_getattro */
0, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */
float_new__doc__, /* tp_doc */
0, /* tp_traverse */
0, /* tp_clear */
float_richcompare, /* tp_richcompare */
0, /* tp_weaklistoffset */
0, /* tp_iter */
0, /* tp_iternext */
float_methods, /* tp_methods */
0, /* tp_members */
float_getset, /* tp_getset */
0, /* tp_base */
0, /* tp_dict */
0, /* tp_descr_get */
0, /* tp_descr_set */
0, /* tp_dictoffset */
0, /* tp_init 对象创建相关(tp_init 函数指针在这为空,因为float对象简单,只需要tp_new赋值就行)*/
0, /* tp_alloc 对象创建相关*/
float_new, /* tp_new 对象创建相关*/
};

PyFloat_Type 中保存了很多关于浮点对象的 元信息

PyFloat_Type 很重要,作为浮点 类型对象 ,它决定了浮点 实例对象 的 生死和行为

对象的创建#

调用类型对象 float 创建实例对象: Python 执行的是 type 类型对象中的 tp_call 函数。 tp_call 函数进而调用 float 类型对象的 tp_new 函数创建实例对象, 再调用 tp_init 函数对其进行初始化

除了通用的流程, Python 为内置对象实现了对象创建 API ,简化调用,提高效率:

1
2
3
4
5
PyObject *
PyFloat_FromDouble(double fval); /*通过浮点值创建浮点对象*/

PyObject *
PyFloat_FromString(PyObject *v); /*通过字符串对象创建浮点对象*/

PyFloat_FromDouble 为例,特化的对象创建流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
PyObject *
PyFloat_FromDouble(double fval)
{
PyFloatObject *op = free_list;
/*为对象 分配内存空间,优先使用空闲对象缓存池 */
if (op != NULL) {
free_list = (PyFloatObject *) Py_TYPE(op);
numfree--;
}
/*空闲对象缓存池为空,调用PyObject_MALLOC申请内存 */
else {
op = (PyFloatObject*) PyObject_MALLOC(sizeof(PyFloatObject));
if (!op)
return PyErr_NoMemory();
}
/* Inline PyObject_New */
(void)PyObject_INIT(op, &PyFloat_Type);

/*将 ob_fval 字段初始化为指定 浮点值 */
op->ob_fval = fval;
return (PyObject *) op;
}

上面用到的宏 PyObject_INIT 在头文件 Include/objimpl.h 中定义为:

1
2
#define PyObject_INIT(op, typeobj) \
( Py_TYPE(op) = (typeobj), _Py_NewReference((PyObject *)(op)), (op) ) //前半部分调用 Py_TYPE(op) 初始化 对象类型 字段 ob_type,后面语句初始化 引用计数 字段 ob_refcnt

上面提到的宏定义 Py_TYPE,位于 Include/object.h 头文件:

1
#define Py_TYPE(ob) (((PyObject*)(ob))&ob_type)
它的作用是:将 给定对象的类型对象取出, 返回对象的ob_type字段

宏 _Py_NewReference,在 Include/Object.h 中定义:

1
2
3
4
#define _Py_NewReference(op) (                          \
_Py_INC_TPALLOCS(op) _Py_COUNT_ALLOCS_COMMA \
_Py_INC_REFTOTAL _Py_REF_DEBUG_COMMA \
Py_REFCNT(op) = 1) /*将对象引用计数初始化为 1*/

# 对象的销毁 当对象不再需要时, Python 通过 Py_DECREF 或者 Py_XDECREF 宏减少引用计数; 当引用计数降为 0 时, Python 通过 **_Py_Dealloc** 宏回收对象:
1
2
3
#define _Py_Dealloc(op) (                               \
_Py_INC_TPFREES(op) _Py_COUNT_ALLOCS_COMMA \
(*Py_TYPE(op)->tp_dealloc)((PyObject *)(op))) /*调用类型对象 **PyFloat_Type 中的 tp_dealloc 函数指针*/
因此,实际调用的函数是 float_dealloc (代码在下一小节 空闲对象缓存池 中解析):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void
float_dealloc(PyFloatObject *op)
{
if (PyFloat_CheckExact(op)) {
/*空闲对象缓存池满了,直接销毁*/
if (numfree >= PyFloat_MAXFREELIST) {
PyObject_FREE(op);
return;
}
numfree++;
Py_TYPE(op) = (struct _typeobject *)free_list;
free_list = op;
}
else
Py_TYPE(op)->tp_free((PyObject *)op);
}
总结起来,对象从创建到销毁整个生命周期所涉及的 关键函数、宏及调用关系 如下:

空闲对象缓存池#

浮点运算背后涉及 大量临时对象创建以及销毁 ,以下面计算为例:

1
>>> area = pi * r ** 2
这个语句首先计算半径 r ** 2,中间结果由一个临时对象来保存,假设是 t ; 然后计算圆周率 pi 与 t 的乘积,得到最终结果并赋值给变量 area ; 最后,销毁临时对象 t 。 这么简单的语句,背后居然都隐藏着一个 临时对象的创建以及销毁操作!

创建对象时需要分配内存,销毁对象时又需要回收内存。 大量临时对象创建销毁 ,意味着 大量内存分配回收操作 ,这显然是是不可接受的。

因此 Python 在浮点对象销毁后,并不急于回收内存,而是将对象放入一个 空闲链表 。 后续需要创建浮点对象时,先到空闲链表中取,省去分配内存的开销。

浮点对象的空闲链表同样在 Objects/floatobject.c 中定义:
1
2
3
4
5
#ifndef PyFloat_MAXFREELIST
#define PyFloat_MAXFREELIST 100 /*该宏 限制空闲链表的 最大长度 ,避免占用过多内存*/
#endif
static int numfree = 0; /*维护空闲链表 当前长度*/
static PyFloatObject *free_list = NULL; /*指向空闲链表 头节点 的指针*/
为了保持简洁, Python 把 ob_type 字段当作 next 指针来用,将 空闲对象串成链表

因此创建浮点对象时,可以从 链表中取出空闲对象,省去 申请内存的开销! 以 PyFloat_FromDouble 为例:

1
2
3
4
5
6
7
8
PyFloatObject *op = free_list;                  /*op指向第一个 空闲对象*/
if (op != NULL) {
free_list = (PyFloatObject *) Py_TYPE(op); /*free_list指向第一个 空闲对象(op)的ob_type所指向的 下一个空闲对象(相当于链表的头部删除)*/
numfree--; /*更新空闲链表维护的数量*/
} else {
op = (PyFloatObject*) PyObject_MALLOC(sizeof(PyFloatObject)); /*free_list为空时,重新分配内存*/
// ...
}

对象销毁时, Python 将其缓存在空闲链表中,以备后用。考察 float_dealloc 函数:

1
2
3
4
5
6
7
if (numfree >= PyFloat_MAXFREELIST)  {   /*销毁时,判断free_list是否达到最大容量*/
PyObject_FREE(op); /*回收对象内*/
return;
}
numfree++;
Py_TYPE(op) = (struct _typeobject *)free_list; /*op的ob_type指向当前 第一个空闲对象*/
free_list = op; /* free_list指向op(相当于链表的头部插入)*/
空闲对象缓存池在 提高对象分配效率 方面发挥着至关重要的作用。

对象的行为#

PyFloat_Type 中定义了很多函数指针,包括 tp_repr 、 tp_str 、 tp_hash 等。 这些函数指针将一起决定 float 对象的行为,例如 tp_hash 函数决定浮点哈希值的计算:

1
2
3
>>> pi = 3.14
>>> hash(pi)
322818021289917443
tp_hash 函数指针指向 float_hash 函数,实现了 针对浮点对象的哈希值算法
1
2
3
4
5
static Py_hash_t
float_hash(PyFloatObject *v)
{
return _Py_HashDouble(v->ob_fval);
}
## 数值操作集 由于加减乘除等数值操作很常见, Python 将其抽象成数值操作集 PyNumberMethods 。 数值操作集 PyNumberMethods 在头文件 Include/object.h 中定义:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct {
/* Number implementations must check *both*
arguments for proper type and implement the necessary conversions
in the slot functions themselves. */

binaryfunc nb_add;
binaryfunc nb_subtract;
binaryfunc nb_multiply;
binaryfunc nb_remainder;
binaryfunc nb_divmod;
ternaryfunc nb_power;
unaryfunc nb_negative;
// ...

binaryfunc nb_inplace_add;
binaryfunc nb_inplace_subtract;
binaryfunc nb_inplace_multiply;
binaryfunc nb_inplace_remainder;
ternaryfunc nb_inplace_power;
//...
} PyNumberMethods;
PyNumberMethods 定义了各种数学算子的处理函数,数值计算最终由这些函数执行。 处理函数根据参数个数可以分为: 一元函数 ( unaryfunc )、 二元函数 ( binaryfunc )等。

  • 一元函数 ( unaryfunc ): 需要传入一个参数的函数。
  • 二元函数 ( binaryfunc): 需要传入两个参数的函数。

回到 Objects/floatobject.c 观察浮点对象数值操作集 float_as_number 是如何初始化的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static PyNumberMethods float_as_number = {
float_add, /* nb_add */
float_sub, /* nb_subtract */
float_mul, /* nb_multiply */
float_rem, /* nb_remainder */
float_divmod, /* nb_divmod */
float_pow, /* nb_power */
(unaryfunc)float_neg, /* nb_negative */
// ...

0, /* nb_inplace_add */
0, /* nb_inplace_subtract */
0, /* nb_inplace_multiply */
0, /* nb_inplace_remainder */
0, /* nb_inplace_power */
// ...
};
以加法为例,以下语句在 Python 内部最终由 float_add 函数执行:
1
2
3
4
>>> a = 1.5
>>> b = 1.1
>>> a + b
2.6
float_add 是一个 二元函数 ,位于 Objects/floatobject.h 中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static PyObject *
float_add(PyObject *v, PyObject *w)
{
double a,b;
//将两个参数对象转化成浮点值,CONVERT_TO_DOUBLE是一个宏,将PyFloatObject里面的ob_fval抽出来给double变量
CONVERT_TO_DOUBLE(v, a);
CONVERT_TO_DOUBLE(w, b);
PyFPE_START_PROTECT("add", return 0)

//对两个浮点值求和
a = a + b;
PyFPE_END_PROTECT(a)

//创建一个新浮点对象保存计算结果并返回
return PyFloat_FromDouble(a);
}
浮点数计算一般都遵循 IEEE-754标准,如果计算时出现了错误,那么需要 将IEEE-754异常转换成Python中的异常,而 PyFPE_START_PROTECT和PyFPE_END_PROTECT这两个宏就是用来干这件事情的。 它们的定义在Include/pyfpe.h中,并且Python3.9的时候会被删除掉。

所以如果是 C中的两个浮点数相加,直接a + b就可以了,编译之后就是一条简单的机器指令,然而 Python则需要额外做很多其它工作。从一个简单的加法上面就可以看出来Python为什么会比C慢几十倍了。

reference#

  1. 小试牛刀,解剖浮点对象 float
  2. 浮点数的底层实现

cocosApi学习#

这篇博客记录一下工作中遇到的Cocos相关api。

EventListenerTouchOneByOne::create()#

单点触摸监听

1
listener = cc.EventListenerTouchOneByOne.create()

cc.fadeTo#

修改 透明度到指定值

容器动作#

顺序动作 cc.sequence#

顺序动作可以让一系列子动作按顺序一个个执行

1
2
3
// 让节点左右来回移动
var seq = cc.sequence(cc.moveBy(0.5, 200, 0), cc.moveBy(0.5, -200, 0));
node.runAction(seq);

同步动作 cc.spawn#

同步动作可以同步执行对一系列子动作,子动作的执行结果会叠加起来修改节点的属性。示例:

1
2
3
// 让节点在向上移动的同时缩放
var spawn = cc.spawn(cc.moveBy(0.5, 0, 50), cc.scaleTo(0.5, 0.8, 1.4));
node.runAction(spawn);

重复动作 cc.repeat#

重复动作用来多次重复一个动作。示例:

1
2
3
4
5
6
7
// 让节点左右来回移动,并重复 5 次
var seq = cc.repeat(
cc.sequence(
cc.moveBy(2, 200, 0),
cc.moveBy(2, -200, 0)
), 5);
node.runAction(seq);

ccc.CallFunc.create(func)#

调用当前不带参数的函数

cc.DelayTime.create(float delaytime)#

延迟当前的action

1
2
3
4
node.runAction(cc.Sequence.create([cc.DelayTime.create(1), cc.CallFunc.create(callback)]))

def callback():
print "just test"

ccui#

ccui.TOUCH_EVENT_MOVED当前为触屏移动状态#

1
2
if ccui.TOUCH_EVENT_MOVED == evtType:
pass

ccui.TOUCH_EVENT_ENDED 当前为触屏抬起状态#

1
2
if evtType == ccui.TOUCH_EVENT_ENDED:
pass

button按钮#

button图片设置#

1
2
btn.loadTextureNormal("....png")  # 正常状态
btn.loadTexturePressed("....png") # 按下状态

这篇文章用来记录工作和学习过程中遇到的一些python常用api方法。

os模块#

os.walk()#

1
os.walk(top[, topdown=True[, onerror=None[, followlinks=False]]])

参数 top -- 是你所要遍历的目录的地址, 返回的是一个三元组(root,dirs,files)。 - root 所指的是当前正在遍历的这个文件夹的本身的地址 - dirs 是一个 list ,内容是该文件夹中所有的目录的名字(不包括子目录) - files 同样是 list , 内容是该文件夹中所有的文件(不包括子目录)

topdown --可选,为 True,则优先遍历 top 目录,否则优先遍历 top 的子目录(默认为开启)。如果 topdown 参数为 True,walk 会遍历top文件夹,与top 文件夹中每一个子目录。

onerror -- 可选,需要一个 callable 对象,当 walk 需要异常时,会调用。 followlinks -- 可选,如果为 True,则会遍历目录下的快捷方式(linux 下是软连接 symbolic link )实际所指的目录(默认关闭),如果为 False,则优先遍历 top 的子目录。

os.getenv(key, default = None)#

返回环境变量键的值(如果存在),否则返回默认值。

os.path.abspath(file)#

  • 获取的当前执行脚本的完整路径
  • 只有当在脚本中执行的时候,os.path.abspath(file)才会起作用,因为该命令是获取的当前执行脚本的完整路径,如果在交互模式或者terminate 终端中运行会报没有__file__这个错误

os.path.dirname(path)#

去掉文件名,返回目录

os.environ.get()#

是python中os模块获取环境变量的一个方法

getattr(object, name[, default])#

getattr() 函数用于返回一个对象属性值。 - object -- 对象。 - name -- 字符串,对象属性。 - default -- 默认返回值,如果不提供该参数,在没有对应属性时,将触发 AttributeError。

shutil.copyfile(src, dst)#

复制文件内容(不包含元数据)从src到dstdst必须是完整的目标文件名; 如果src和dst是 同一文件,就会引发错误shutil.Errordst必须是可写的,否则将引发异常IOError。如果dst已经存在,它会被替换。

@property#

Python内置的@property装饰器就是负责把一个方法变成属性调用的.

1
2
3
4
5
6
7
8
9
10
11
12
13
class Student(object):

@property
def score(self):
return self._score

@score.setter
def score(self, value):
if not isinstance(value, int):
raise ValueError('score must be an integer!')
if value < 0 or value > 100:
raise ValueError('score must between 0 ~ 100!')
self._score = value
@property的实现比较复杂,我们先考察如何使用。把一个getter方法变成属性,只需要加上@property就可以了,此时,@property本身又创建了另一个装饰器@score.setter,负责把一个setter方法变成属性赋值,于是,我们就拥有一个可控的属性操作:
1
2
3
4
5
6
7
8
>>> s = Student()
>>> s.score = 60 # OK,实际转化为s.set_score(60)
>>> s.score # OK,实际转化为s.get_score()
60
>>> s.score = 9999
Traceback (most recent call last):
...
ValueError: score must between 0 ~ 100!

还可以定义只读属性只定义getter方法,不定义setter方法就是一个只读属性

1
2
3
4
5
6
7
8
9
10
11
12
class Student(object):
@property
def birth(self):
return self._birth

@birth.setter
def birth(self, value):
self._birth = value

@property
def age(self):
return 2014 - self._birth
上面的birth是可读可写的,而age就是一个只读属性

python中函数前添加*以及**的用法#

转自:http://blog.csdn.net/delphiwcdj/article/details/5746560 当要使函数接收元组或字典形式的参数 的时候,有一种特殊的方法,它分别使用*和**前缀 。这种方法在函数需要获取可变数量的参数 的时候特别有用。

[注意] [1] 由于在args变量前有*前缀 ,所有多余的函数参数都会作为一个元组存储在args中 。如果使用的是前缀 ,多余的参数则会被认为是一个字典的健/值对 。 [2] 对于def func(args):,args表示把传进来的位置参数存储在 tuple(元组)args里面。例如,调用func(1, 2, 3) ,args就表示(1, 2, 3)这个元组 。 [3] 对于 def func(args):,args表示把参数 作为字典的健-值对存储在dict(字典)args里面**。例如,调用func(a='I', b='am', c='wcdj') ,args就表示{'a':'I', 'b':'am', 'c':'wcdj'}这个字典 。 [4] 注意普通参数与*和**参数公用的情况,一般将*和**参数放在参数列表最后。

globals()#

以字典类型返回当前位置的全部全局变量

locals() 函数#

locals() 函数会 以字典类型返回当前位置的全部局部变量。 对于函数, 方法, lambda 函式, 类, 以及实现了 call 方法的类实例, 它都返回 True。

lambad函数用法#

####(1)直接赋给一个变量,然后再像一般函数那样调用

1
2
3
4
c=lambda x,y,z:x*y*z
c(2,3,4)

24
当然,也可以在函数后面直接传递实参
1
2
(lambda x:x**2)(3)
9
####(2)将lambda函数作为参数传递给其他函数比如说结合map、filter、sorted、reduce等一些Python内置函数使用,下面举例说明。
1
2
3
4
5
6
7
8
fliter(lambda x:x%3==0,[1,2,3,4,5,6])

[3,6]


squares = map(lambda x:x**2,range(5)
print(lsit(squares))
[0,1,4,9,16]
与sorted函数结合使用,比如:创建由元组构成的列表:
1
a=[('b',3),('a',2),('d',4),('c',1)]
按照第一个元素排序
1
2
sorted(a,key=lambda x:x[0])
[('a',2),('b',3),('c',1),('d',4)]
按照第二个元素排序
1
2
sorted(a,key=lambda x:x[1])
[('c',1),('a',2),('b',3),('d',4)]
与reduce函数结合使用
1
2
3
4
from functools import reduce
print(reduce(lambda a,b:'{},{}'.format(a,b),[1,2,3,4,5,6,7,8,9]))

1,2,3,4,5,6,7,8,9
####(3)嵌套使用将lambda函数嵌套到普通函数中,lambda函数本身做为return的值
1
2
3
4
5
6
def increment(n):
return lambda x:x+n

f=increment(4)
f(2)
6
####(4)字符串联合,有默认值,也可以用x=(lambda...)这种格式
1
2
3
4
x=(lambda x='Boo',y='Too',z='Z00':x+y+z)
print(x('Foo'))

'FooTooZoo'
####(5)在tkinter中定义内联的callback函数
1
2
3
4
5
6
import sys
from tkinter import Button,mainloop

x=Button(text='Press me',command=(lambda :sys.stdout.write('Hello,World\n')))
x.pack()
x.mainloop()
这段代码还是挺有意思的,希望小伙伴们可以复制粘贴运行一下哈。 ####(6)判断字符串是否以某个字母开头有
1
2
3
4
5
Names = ['Anne', 'Amy', 'Bob', 'David', 'Carrie', 'Barbara', 'Zach']
B_Name= filter(lambda x: x.startswith('B'),Names)
print(B_Name)

['Bob', 'Barbara']
####(7)求两个列表元素的和
1
2
3
4
5
a = [1,2,3,4]
b = [5,6,7,8]
print(list(map(lambda x,y:x+y, a,b)))

[6,8,10,12]
####(8)求字符串每个单词的长度
1
2
3
4
5
sentence = "Welcome To Beijing!"
words = sentence.split()
lengths = map(lambda x:len(x),words)
print(list(lengths))
[7,2,8]

for else结构#

for else 结构还是第一次见,于是记录一下。如果for循环 正常结束,else中语句执行。如果是 break的,则 不执行

简单使用场景:寻找100以内的素数之和 (这个应该是较暴力算法,这里不讨论算法问题)

1
2
3
4
5
6
7
8
sum=0
for n in range(2,100):
for i in range(2,n):
if n%i==0: # 不是素数
break
else:
sum+=n # 加上素数
print(sum)

善用三目运算#

1
num = 3 if res > 3 else 1

接受动态参数sys.argv#

1
2
3
4
5
6
7
8
# test.py

import sys
def main(argv)
print(argv)

if __name__ == '__main__':
main(sys.argv)
1
python test.py 1 2 3 4 5

binascii --- 二进制和 ASCII 码互转#

binascii介绍 ### 返回二进制数据 data 的十六进制表示形式

1
2
binascii.b2a_hex(data[, sep[, bytes_per_sep=1]])
binascii.hexlify(data[, sep[, bytes_per_sep=1]])
data 的每个字节都被转换为相应的2位十六进制表示形式。因此返回的字节对象的长度是 data 的两倍。

返回由十六进制字符串 hexstr 表示的二进制数据#

1
2
binascii.a2b_hex(hexstr)
binascii.unhexlify(hexstr)

此函数功能与 b2a_hex() 相反。 hexstr 必须包含 偶数个十六进制数字(可以是大写或小写),否则会引发 Error 异常。

zlip#

  • 字符串:使用 zlib.compress可以压缩字符串。使用 zlib.decompress可以解压字符串。

  • 数据流:压缩:compressobj,解压:decompressobj

cPickle#

cPickle.dump#

将python对象序列化

1
cPickle.dump(obj, file, protocol=None, *, fix_imports=True, buffer_callback=None)

cPickle.load#

载入本地文件,恢复python对象

1
>>> data = cPickle.load(open("test\\data.pkl","rb"))

cPickle.dumps#

将python对象序列化保存到一个字符串变量中

1
>>> data_string = cPickle.dumps(data)

cPickle.loads#

从字符串变量中载入python对象

1
>>> data = cPickle.loads(data_string)

pyflakes#

检查加载字符串代码是否有错

1
2
3
from pyflakes.api import checkCode
a = f.read()
checkCode(a, my_Reporter(sys.stdout, sys.stderr)) # my_Reporter是pyflakes.reporter的拓展

RPC 框架原理#

RPC 框架的目标就是让远程服务调用更加简单、透明,RPC 框架负责屏蔽底层的传输方式(TCP 或者 UDP)、序列化方式(XML/Json/ 二进制)和通信细节。服务调用者可以像调用本地接口一样调用远程的服务提供者,而不需要关心底层通信细节和调用过程。

业界主流的 RPC 框架整体上分为三类:

  • 支持多语言的 RPC 框架,比较成熟的有 Google 的 gRPC、Apache(Facebook)的 Thrift
  • 只支持特定语言的 RPC 框架,例如新浪微博的 Motan
  • 支持服务治理等服务化特性的分布式服务框架,其底层内核仍然是 RPC 框架, 例如阿里的 Dubbo。

gRPC 简介#

gRPC 是一个高性能、开源和通用的 RPC 框架,面向服务端和移动端,基于 HTTP/2 设计。

gRPC 客户端和服务端可以在多种环境中运行和交互 - 从 google 内部的服务器到你自己的笔记本,并且可以用任何 gRPC 支持的语言来编写。所以,你可以很容易地用 Java 创建一个 gRPC 服务端,用 Go、Python、Ruby 来创建客户端。此外,Google 最新 API 将有 gRPC 版本的接口,使你很容易地将 Google 的功能集成到你的应用里。

gRPC 特点 - 语言中立,支持多种语言; - 基于 IDL 文件定义服务,通过 proto3 工具生成指定语言的数据结构、服务端接口以及客户端 Stub; - 通信协议基于标准的 HTTP/2 设计,支持双向流、消息头压缩、单 TCP 的多路复用、服务端推送等特性,这些特性使得 gRPC 在移动端设备上更加省电和节省网络流量; - 序列化支持 PB(Protocol Buffer)和 JSON,PB 是一种语言无关的高性能序列化框架,基于 HTTP/2 + PB, 保障了 RPC 调用的高性能。

gRPC有什么好处以及在什么场景下需要用gRPC#

gRPC vs. Restful API gRPC和restful API都提供了一套通信机制,用于server/client模型通信,而且它们都使用http作为底层的传输协议(严格地说, gRPC使用的http2.0,而restful api则不一定)。不过gRPC还是有些特有的优势,如下:

  • gRPC可以通过protobuf来定义接口,从而可以有更加严格的接口约束条件。
  • 通过protobuf可以将数据序列化为二进制编码,这会大幅减少需要传输的数据量,从而大幅提高性能
  • gRPC可以方便地支持流式通信(理论上通过http2.0就可以使用streaming模式, 但是通常web服务的restful api似乎很少这么用,通常的流式数据应用如视频流,一般都会使用专门的协议如HLS,RTMP等,这些就不是我们通常web服务了,而是有专门的服务器应用。)
  • proto文件生成目标代码,简单易用
  • 序列化反序列化直接对应程序中的数据类,不需要解析后在进行映射(XML,JSON都是这种方式)
  • 支持向前兼容(新加字段采用默认值)和向后兼容(忽略新加字段),简化升级
  • Netty等一些框架集成

缺点:

  • 1、GRPC尚未提供连接池,需要自行实现
  • 2、尚未提供“服务发现”、“负载均衡”机制
  • 3、因为基于HTTP2,绝大部多数HTTP Server、Nginx都尚不支持,即Nginx不能将GRPC请求作为HTTP请求来负载均衡,而是作为普通的TCP请求。(nginx1.9版本已支持)
  • 4、Protobuf二进制可读性差(貌似提供了Text_Fromat功能)
  • 5、默认不具备动态特性(可以通过动态定义生成消息类型或者动态编译支持)

使用 protocol buffers#

gRPC 默认使用 protocol buffers(protobuf),这是 Google 开源的一套成熟的结构数据序列化机制(当然也可以使用其他数据格式如 JSON)。正如你将在下方例子里所看到的,你用 proto files 创建 gRPC 服务,用 protobuf 消息类型来定义方法参数和返回类型。protobuf相关可以看 protobuf简介

基于HTTP 2.0标准设计#

由于gRPC基于HTTP 2.0标准设计,带来了更多强大功能,如多路复用、二进制帧、头部压缩、推送机制。这些功能给设备带来重大益处,如节省带宽、降低TCP连接次数、节省CPU使用等。gRPC既能够在客户端应用,也能够在服务器端应用,从而以透明的方式实现两端的通信和简化通信系统的构建

HTTP 版本分为HTTP 1.X、 HTTP 2.0,其中HTTP 1.X是当前使用最广泛的HTTP协议,HTTP 2.0称为超文本传输协议第二代。HTTP 1.X定义了四种与服务器交互的方式,分别为:GET、POST、PUT、DELETE,这些在HTTP 2.0中均保留。我们再来看看HTTP 2.0的新特性:

1.双向流、多路复用#

在HTTP 1.X协议中,客户端在同一时间访问同一域名的请求数量是有限制的,当超过阈值时请求会被阻断,但是这种情况在HTTP 2.0中将被忽略。由于HTTP 1.X传输的是纯文本数据,传输体积较大,而HTTP 2.0传输的基本单元为帧,每个帧都包含消息,并且由于HTTP 2.0允许同时通过一条连接发起多个“请求-响应”消息,无需建立多个TCP链接的同时实现多条流并行,提高吞吐性能,并且在一个连接内对多个消息进行优先级的管理和流控。

2.二进制帧#

相对于HTTP 1.X的纯文本传输,HTTP 2.0传输的是二进制数据,与Protocol Buffers相辅相成。使得传输数据体积小、负载低,保持更加紧凑和高效

3.头部压缩#

因为HTTP是无状态协议,对于业务的处理没有记忆能力,每一次请求都需要携带设备的所有细节,特别是在头部都会包含大量的重复数据,对于设备来说就是在不断地做无意义的重复性工作。HTTP 2.0中使用“头表”来跟踪之前发送的数据,对于相同的数据将不再使用重复请求和发送,进而减少数据的体积


gRPC有四种通信方式:#

1、 Simple RPC#

简单rpc 这就是一般的rpc调用,一个请求对象对应一个返回对象 proto语法:

1
rpc simpleHello(Person) returns (Result) {}
### 2、 Server-side streaming RPC 服务端流式rpc 一个请求对象,服务端可以传回多个结果对象 proto语法
1
rpc serverStreamHello(Person) returns (stream Result) {}
### 3、 Client-side streaming RPC 客户端流式rpc 客户端传入多个请求对象,服务端返回一个响应结果 proto语法
1
rpc clientStreamHello(stream Person) returns (Result) {}
### 4、 Bidirectional streaming RPC 双向流式rpc 结合客户端流式rpc和服务端流式rpc,可以传入多个对象,返回多个响应对象 proto语法
1
rpc biStreamHello(stream Person) returns (stream Result) {}
--- ## 服务端创建流程 gRPC 服务端创建采用 Build 模式,对底层服务绑定、transportServer 和 NettyServer 的创建和实例化做了封装和屏蔽,让服务调用者不用关心 RPC 调用细节,整体上分为三个过程:

  • 创建 Netty HTTP/2 服务端;
  • 将需要调用的服务端接口实现类注册到内部的 Registry 中,RPC 调用时,可以根据 RPC 请求消息中的服务定义信息查询到服务接口实现类;
  • 创建 gRPC Server,它是 gRPC 服务端的抽象,聚合了各种 Listener,用于 RPC 消息的统一调度和处理。

gRPC 服务端创建关键流程分析:#

  • NettyServer 实例创建:gRPC 服务端创建,首先需要初始化 NettyServer,它是 gRPC 基于 Netty 4.1 HTTP/2 协议栈之上封装的 HTTP/2 服务端。NettyServer 实例由 NettyServerBuilder 的 buildTransportServer 方法构建,NettyServer 构建完成之后,监听指定的 Socket 地址,即可实现基于 HTTP/2 协议的请求消息接入
  • 绑定 IDL 定义的服务接口实现类:gRPC 与其它一些 RPC 框架的差异点是服务接口实现类的调用并不是通过动态代理和反射机制,而是通过 proto 工具生成代码,在服务端启动时,将服务接口实现类实例注册到 gRPC 内部的服务注册中心上。请求消息接入之后,可以根据服务名和方法名,直接调用启动时注册的服务实例,而不需要通过反射的方式进行调用,性能更优。
  • gRPC 服务实例(ServerImpl)构建:ServerImpl 负责整个 gRPC 服务端消息的调度和处理,创建 ServerImpl 实例过程中,会对服务端依赖的对象进行初始化,例如 Netty 的线程池资源、gRPC 的线程池、内部的服务注册类(InternalHandlerRegistry)等,ServerImpl 初始化完成之后,就可以调用 NettyServer 的 start 方法启动 HTTP/2 服务端,接收 gRPC 客户端的服务调用请求

gRPC HelloWorld实例详解#

gRPC的使用通常包括如下几个步骤:

  1. 通过protobuf来定义接口和数据类型
  2. 使用gRPC protobuf生成工具生成对应语言的库函数
  3. 编写gRPC server端代码
  4. 编写gRPC client端代码 下面来通过一个实例来详细讲解上述的三步。 下边的hello world实例完成之后,其目录结果如下:

1. 定义接口和数据类型#

通过protobuf定义接口和数据类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
syntax = "proto3";

package rpc_package;

// 客户端发送rpc方法,response = stub.SayHello(HelloRequest(name='eric'))
// 服务端返回response,是HelloReply类型

service HelloWorldService {
// define the interface and data type
rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// define the data type of request
message HelloRequest {
string name = 1;
}

// define the data type of response
message HelloReply {
string message = 1;
}

2.使用gRPC protobuf生成工具生成对应语言的库函数#

1
python -m grpc_tools.protoc -I=./protos --python_out=./rpc_package --grpc_python_out=./rpc_package

这个指令会自动生成rpc_package文件夹中的helloworld_pb2.py和helloworld_pb2_grpc.py,但是不会自动生成__init__.py文件,需要我们手动添加

3. 编写gRPC server端代码#

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
39
40
#!/usr/bin/env python
# -*-coding: utf-8 -*-

from concurrent import futures
import grpc
import logging
import time

from rpc_package.helloworld_pb2_grpc import add_HelloWorldServiceServicer_to_server, \
HelloWorldServiceServicer
from rpc_package.helloworld_pb2 import HelloRequest, HelloReply


class Hello(HelloWorldServiceServicer):

# 这里实现我们定义的接口
def SayHello(self, request, context):
return HelloReply(message='Hello, %s!' % request.name)


def serve():
# 这里通过thread pool来并发处理server的任务
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))

# 将对应的任务处理函数添加到rpc server中
add_HelloWorldServiceServicer_to_server(Hello(), server)

# 这里使用的非安全接口,世界gRPC支持TLS/SSL安全连接,以及各种鉴权机制
server.add_insecure_port('[::]:50000')
server.start()
try:
while True:
time.sleep(60 * 60 * 24)
except KeyboardInterrupt:
server.stop(0)


if __name__ == "__main__":
logging.basicConfig()
serve()

4.gRPC client端代码#

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
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import print_function
import logging

import grpc
from rpc_package.helloworld_pb2 import HelloRequest, HelloReply
from rpc_package.helloworld_pb2_grpc import HelloWorldServiceStub

def run():
# 使用with语法保证channel自动close
with grpc.insecure_channel('localhost:50000') as channel:
# 客户端通过stub来实现rpc通信
# 传入通信channel
stub = HelloWorldServiceStub(channel)

# 客户端必须使用定义好的类型,这里是HelloRequest类型
# 客户端发/请求
response = stub.SayHello(HelloRequest(name='eric'))
print ("hello client received: " + response.message)

if __name__ == "__main__":
logging.basicConfig()
run()

demo#

运行server端代码

1
python hello_server.py
接着执行client端代码如下:
1
2
➜  grpc_test python hello_client.py
hello client received: Hello, eric!

reference#

  1. gRPC官网
  2. grpc原理
  3. grpc特性分析
  4. gRPC 官方文档中文版
  5. grpc应用详解与实例剖析

目录#

[TOC]

Protobuf是什么#

Protobuf全称是Google Protocol Buffer,是一种高效轻便的结构化数据存储方式,可用于网络通信、数据存储等。

其具有以下优点:

  • 平台无关、语言无关
  • 支持Java, C++, Python等多种语言,支持多平台。
  • 轻便高效,比XML更小(3~10倍),更快(20 ~ 100倍),更为简单。
  • 扩展性,兼容性好
  • 序列化数据结构的协议,可以更新数据结构,而不影响和破坏原有的旧程序。

序列化#

将数据结构或对象转换成能够被存储和传输(例如网络传输)的格式(网络传输传输的是二进制数据),同时应当要保证这个序列化结果在之后(可能是另一个计算环境中)能够被重建回原来的数据结构或对象

protobuf语法介绍#

目前有Protobuf2和Protobuf3。

protobuf2语法简介#

.proto文件中数据类型可以分为两大类:

  • 复合数据类型包括:枚举和message类型
  • 标准数据类型包含:整型,浮点,字符串

数据类型前面修饰词:

  • required: 必须赋值,不能为空,否则该条message会被认为是“uninitialized”。除此之外,“required”字段跟“optional”字段并无差别。
  • optional:字段可以赋值,也可以不赋值。假如没有赋值的话,会被赋上默认值。
  • repeated: 该字段可以重复任意次数,包括0次。重复数据的顺序将会保存在protocol buffer中,将这个字段想象成一个可以自动设置size的数组就可以了。

注:每个字段要给数字 该Number是用来标记该字段在序列化后的二进制数据中所在的field,每个字段的Number在message内部都是独一无二的。也不能进行改变,否则数据就不能正确的解包

关于 proto2 定义 message 消息的更多语法细节,例如具有支持哪些类型,字段编号分配、import 导入定义,reserved 保留字段等知识请参阅 ProtoBuf 官方文档(二)- 语法指引(proto2)

关于定义时的一些规范请参阅 [翻译] ProtoBuf 官方文档(四)- 规范指引

protobuf3语法介绍#

  • 字段前取消了required和optional两个关键字,目前可用的只有repeated关键字。

  • 不可以设置默认值了。

    1. string默认为空串
    2. 枚举默认为第一个枚举定义的第一个值。并且必须是0,必须有有一个0值,我们可以用这个0值作为默认值。这个零值必须为第一个元素,为了兼容proto2语义,枚举类的第一个值总是默认值。
    3. bytes默认为空bytes
    4. bool默认为false
    5. 数字类型默认为0
  • protoType类型如下: double、float、int32、int64、uint32、uint64、sint32、sint64、fixed32、fixed64、sfixed32、sfixed64、bool、string、bytes

  • 分配标识号 正如你所见,在消息定义中,每个字段都有唯一的一个数字标识符。这些标识符是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变。注:[1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留 [1,15]之内的标识号。切记:要为将来有可能添加的、频繁出现的标识号预留一些标识号。

  • 标识号区间[1,2^29 - 1]。不可以使用其中的[19000-19999](从FieldDescriptor::kFirstReservedNumber 到 FieldDescriptor::kLastReservedNumber)的标识号, Protobuf协议实现中对这些进行了预留。如果非要在.proto文件中使用这些预留标识号,编译时就会报警。同样你也不能使用早期保留的标识号。

  • 指定字段规则 所指定的消息字段修饰符必须是如下之一: singular:一个格式良好的消息应该有0个或者1个这种字段(但是不能超过1个)。 repeated:在一个格式良好的消息中,这种字段可以重复任意多次(包括0次)。重复的值的顺序会被保留。 在proto3中,repeated的标量域默认情况下使用packed。

安装#

使用Python的话简便的安装方法如下(linux)

1
2
pip install protobuf    # 安装protobuf库
sudo apt-get install protobuf-compiler # 安装protobuf编译器

使用#

目前有Protobuf2和Protobuf3,本文以Protobuf3为例。

第一步,创建.proto文件,定义数据结构#

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
//指定正在使用proto3语法:如果没有指定这个,编译器会使用proto2
//这个指定语法行必须是文件的非空非注释的第一个行
syntax = "proto3";
package tutorial;

message AddressBook {
repeated Person people = 1;
}

message Person {
string name = 1;
int32 id = 2;
string email = 3;
float money = 4;
bool work_status = 5;

repeated PhoneNumber phones = 6;
MyMessage maps = 7;

}

message PhoneNumber {
string number = 1;
PhoneType type = 2;
}

enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}


message MyMessage {
map<int32, int32> mapfield = 1;
}
}

第二步,protoc 编译 .proto 文件生成读写接口#

我们在 .proto 文件中定义了数据结构,这些数据结构是面向开发者和业务程序的,并不面向存储和传输。

当需要把这些数据进行存储或传输时,就需要将这些结构数据进行序列化、反序列化以及读写。那么如何实现呢?答案就是通过 protoc 这个编译器。

利用protoc.exe编译proto文件,cmd切换到当前目录,执行以下命令:

1
2
3
4
5
// $SRC_DIR: .proto 所在的源目录
// --python_out: 生成 python 代码
// $DST_DIR: 生成代码的目标目录
// xxx.proto: 要针对哪个 proto 文件生成接口代码
protoc -I=$SRC_DIR --python_out=$DST_DIR xxx.proto
这里我们使用如下编译语句
1
protoc -I=. --python_out=./ addressbook.proto

编译好之后你就会在目标目录里面看到输出的结果文件,如下:addressbook_pb2.py

第三步,编译.py文件,进行序列化和凡序列化#

add_person.py

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
#! /usr/bin/env python
from tutorial import addressbook_pb2

address_book = addressbook_pb2.AddressBook()
person = address_book.people.add()

person.id = 1
person.name = "safly"
person.email = "safly@qq.com"
person.money = 1000.11
person.work_status = True

phone_number = person.phones.add()
phone_number.number = "123456"
phone_number.type = addressbook_pb2.MOBILE

maps = person.maps
maps.mapfield[1] = 1
maps.mapfield[2] = 2

#序列化
serializeToString = address_book.SerializeToString()
print(serializeToString,type(serializeToString))



address_book.ParseFromString(serializeToString)

for person in address_book.people:
print("p_id{},p_name{},p_email{},p_money{},p_workstatu{}"
.format(person.id,person.name,person.email,person.money,person.work_status))

for phone_number in person.phones:
print(phone_number.number,phone_number.type)


for key in person.maps.mapfield:
print(key,person.maps.mapfield[key])
编译该py文件,输出结果如下:
1
2
3
4
5
6
b'\n6\n\x05safly\x10\x01\x1a\x0csafly@qq.com%\n\x07zD(\x012\x08\n\x06123456:\x0c\n\x04\x08\x01\x10\x01\n\x04\x08\x02\x10\x02' <class 'bytes'>

p_id1,p_namesafly,p_emailsafly@qq.com,p_money1000.1099853515625,p_workstatuTrue
123456 0
1 1
2 2
我们就看到了序列化和反序列化的结果

这篇简介就介绍到这,后期会继续

进阶使用#

addressbook.proto内容如下:

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
39
syntax = "proto3";
package tutorial;
//引入外部proto
import "emu.proto";


message AddressBook {
repeated Person people = 1;
}

message Person {
string name = 1;
int32 id = 2;
string email = 3;
float money = 4;
bool work_status = 5;

repeated PhoneNumber phones = 6;
//外部引用map
repeated MyMessage maps = 7;

//内部嵌套message
repeated Hobby hobby = 8;
message Hobby{
string interest = 1;
}

}

message PhoneNumber {
string number = 1;
PhoneType type = 2;
}

enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
我们将addressbook.proto中的repeated MyMessage maps = 7;进行了外部引用, emu.proto如下:

1
2
3
4
5
6
7
syntax = "proto3";
package tutorial;


message MyMessage {
map<int32, int32> mapfield = 1;
}

然后首先对emu.proto进行编译, protoc ./emu.proto --python_out=./

然后会addressbook.proto进行编译 protoc ./addressbook.proto --python_out=./ 然后会默认生成上述截图中的emu_pb2.py、addressbook_pb2.py文件

我们接下来看看add_person.py代码

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
39
40
41
42
43
44
45
46
47
48
49
#! /usr/bin/env python
from tutorial import addressbook_pb2

address_book = addressbook_pb2.AddressBook()
person = address_book.people.add()

person.id = 1
person.name = "safly"
person.email = "safly@qq.com"
person.money = 1000.11

person.work_status = True

phone_number = person.phones.add()
phone_number.number = "123456"
phone_number.type = addressbook_pb2.MOBILE

maps = person.maps.add()
maps.mapfield[1] = 1
maps.mapfield[2] = 2

hobby = person.hobby.add()
hobby.interest = "python"


#序列化
serializeToString = address_book.SerializeToString()
print(serializeToString,type(serializeToString))



address_book.ParseFromString(serializeToString)

for person in address_book.people:
print("p_id{},p_name{},p_email{},p_money{},p_workstatu{}"
.format(person.id,person.name,person.email,person.money,person.work_status))

for phone_number in person.phones:
print(phone_number.number,phone_number.type)
print(person.phones[0].number)


for map in person.maps:
for key in map.mapfield:
print(key,'-------',map.mapfield[key])


for hobby in person.hobby:
print(hobby.interest)
最后输出结果如下:
1
2
3
4
5
6
7
8
9
10
/Users/zhiliao/miniconda3/bin/python /Users/zhiliao/zhiliao/untitled1/tutorial/add_person.py
b'\n@\n\x05safly\x10\x01\x1a\x0csafly@qq.com%\n\x07zD(\x012\x08\n\x06123456:\x0c\n\x04\x08\x01\x10\x01\n\x04\x08\x02\x10\x02B\x08\n\x06python' <class 'bytes'>
p_id1,p_namesafly,p_emailsafly@qq.com,p_money1000.1099853515625,p_workstatuTrue
123456 0
123456
2 ------- 2
1 ------- 1
python

Process finished with exit code 0

reference#

  1. python基础--protobuf的使用(一)
  2. Protobuf学习
  3. Protobuf Python 示例

拓展阅读#

  1. 深入 ProtoBuf - 编码
  2. 深入 ProtoBuf - 序列化源码解析
  3. 深入 ProtoBuf - 反射原理解析

C API#

开始讨论对象创建前,先介绍 Python 提供的 C API 。 Python 是用 C 写成的,对外提供了 C API ,让用户可以从 C 环境中与其交互。 Python 内部也大量使用这些 API ,为了更好研读源码,先系统了解 API 组成结构很有必要。 C API 分为两类: 泛型 API 以及 特型 API

泛型 API#

泛型 API 与类型无关,属于 抽象对象层 ( Abstract Object Layer ),简称 AOL 。 这类 API 参数是 PyObject* ,可处理任意类型的对象, API 内部根据对象类型区别处理。 以对象打印函数为例:

1
int PyObject_Print(PyObject *op, FILE *fp, int flags)

接口第一个参数为待打印对象,可以是任意类型的对象,因此参数类型是 PyObject* 。 Python 内部一般都是通过 PyObject* 引用对象,以达到泛型化的目的。 对于任意类型的对象,均可调用 PyObject_Print 将其打印出来

1
2
3
4
5
6
7
8
// 打印浮点对象
PyObject *fo = PyFloatObject_FromDouble(3.14);
PyObject_Print(fo, stdout, 0);

// 打印整数对象
PyObject *lo = PyFloatObject_FromLong(100);
PyObject_Print(lo, stdout, 0);
PyObject_Print 接口内部根据对象类型,决定如何输出对象。

特型 API#

特型 API 与类型相关,属于 具体对象层 ( Concrete Object Layer ),简称 COL 。 这类 API 只能作用于某种类型的对象,例如浮点对象 PyFloatObject 。 Python 内部为每一种内置对象提供了这样一组 API ,举例如下:

1
PyObject * PyFloat_FromDouble(double fval)
PyFloat_FromDouble 创建一个浮点对象,并将它初始化为给定值 fval 。

对象的创建#

经过前面的理论学习,我们知道对象的 元数据 保存在对应的 类型对象 中,元数据当然也包括 对象如何创建 的信息。 因此,有理由相信 实例对象 由 类型对象 创建。 不管创建对象的流程如何,最终的关键步骤都是 分配内存 。 Python 对 内建对象 是无所不知的,因此可以提供 C API ,直接分配内存并执行初始化。 以 PyFloat_FromDouble 为例,在接口内部为 PyFloatObject 结构体分配内存,并初始化相关字段即可。 对于用户自定义的类型 class Dog(object) , Python 就无法事先提供 PyDog_New 这样的 C API 了。 这种情况下,就只能通过 Dog 所对应的类型对象创建实例对象了。 至于需要分配多少内存,如何进行初始化,答案就需要在 类型对象 中找了。 总结起来,Python 内部一般通过这两种方法创建对象: - 通过 C API ,例如 PyFloat_FromDouble ,多用于内建类型; - 通过类型对象,例如 Dog ,多用于自定义类型; 通过类型对象创建实例对象,是一个更通用的流程,同时支持内置类型和自定义类型。 以创建浮点对象为例,我们还可以通过浮点类型 PyFloat_Type 来创建:

1
2
3
>>> pi = float('3.14')
>>> pi
3.14
例子中我们通过调用类型对象 float ,实例化了一个浮点实例 pi ,对象居然还可以调用!在 Python 中,可以被调用的对象就是 可调用对象

问题来了,可调用对象被调用时,执行什么函数呢? 由于类型对象保存着实例对象的元信息, float 类型对象的类型是 type ,因此秘密应该就隐藏在 type 中。 再次考察 PyType_Type ,我们找到了 tp_call 字段,这是一个 函数指针

1
2
3
4
5
6
7
8
9
10
11
PyTypeObject PyType_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"type", /* tp_name */
sizeof(PyHeapTypeObject), /* tp_basicsize */
sizeof(PyMemberDef), /* tp_itemsize */

// ...
(ternaryfunc)type_call, /* tp_call */

// ...
};
当实例对象被调用时,便执行 tp_call 字段保存的处理函数。 因此, float(‘3.14’) 在 C 层面等价于:
1
PyFloat_Type.ob_type.tp_call(&PyFloat_Type, args, kwargs)
即:
1
PyType_Type.tp_call(&PyFloat_Type, args, kwargs)
最终执行, type_call 函数:
1
type_call(&PyFloat_Type, args, kwargs)
调用参数通过 args 和 kwargs 两个对象传递,先不展开,留到函数机制中详细介绍。 接着围观 type_call 函数,定义于 Include/typeobject.c ,关键代码如下:
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
static PyObject *
type_call(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
PyObject *obj;

// ...
obj = type->tp_new(type, args, kwds); //为对象分配内存
obj = _Py_CheckFunctionResult((PyObject*)type, obj, NULL);
if (obj == NULL)
return NULL;

// ...
type = Py_TYPE(obj); //获取PyType_Type
if (type->tp_init != NULL) {
int res = type->tp_init(obj, args, kwds); //初始化对象
if (res < 0) {
assert(PyErr_Occurred());
Py_DECREF(obj); //引用减一
obj = NULL;
}
else {
assert(!PyErr_Occurred());
}
}
return obj;
}

可以看到,关键的步骤有两个: 1. 调用类型对象 tp_new 函数指针 申请内存 (第 7 行); 2. 必要时调用类型对象 tp_init 函数指针对对象进行 初始化 (第 15 行); 至此,对象的创建过程已经非常清晰了:


总结一下,float 类型对象是 可调用对象 ,调用 float 即可创建实例对象: 1. 调用 float , Python 最终执行其类型对象 type 的 tp_call 函数; 2. tp_call 函数调用 float 的 tp_new 函数为实例对象分配 内存空间 ; 3. tp_call 函数必要时进一步调用 tp_init 函数对实例对象进行 初始化 ;

对象的多态性#

Python 创建一个对象,比如 PyFloatObject ,会分配内存,并进行初始化。 此后, Python 内部 统一通过一个 PyObject* 变量来保存和维护这个对象,而不是通过 PyFloatObject* 变量。 通过 PyObject* 变量保存和维护对象,可以 实现更抽象的上层逻辑,而不用关心对象的实际类型和实现细节。 以对象哈希值计算为例,假设有这样一个函数接口:

1
Py_hash_t PyObject_Hash(PyObject *v);
该函数可以计算任意对象的哈希值,不管对象类型是啥。 例如,计算浮点对象哈希值:
1
2
PyObject *fo = PyFloatObject_FromDouble(3.14);
PyObject_Hash(fo);
对于其他类型,例如整数对象,也是一样的:
1
2
PyObject *lo = PyLongObject_FromLong(100);
PyObject_Hash(lo);
然而,对象类型不同,其行为也千差万别,哈希值计算方法便是如此。 PyObject_Hash 函数如何解决这个问题呢? 到 Object/object.c 中寻找答案:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Py_hash_t
PyObject_Hash(PyObject *v)
{
//类似强制类型转换,但是不像c++一样,用父类指针调用子类就行
//这里的结构体里面指针有ob_type和tp_hash,标记了类型和哈希函数
PyTypeObject *tp = Py_TYPE(v);
if (tp->tp_hash != NULL)
return (*tp->tp_hash)(v);
/* To keep to the general practice that inheriting
* solely from object in C code should work without
* an explicit call to PyType_Ready, we implicitly call
* PyType_Ready here and then check the tp_hash slot again
* 隐式调用PyType_Ready,然后再次检查tp_hash插槽
*/
if (tp->tp_dict == NULL) {
if (PyType_Ready(tp) < 0)
return -1;
if (tp->tp_hash != NULL)
return (*tp->tp_hash)(v);
}
/* Otherwise, the object can't be hashed */
return PyObject_HashNotImplemented(v);
}
函数先通过 ob_type 指针找到对象的类型 (第 4 行); 然后通过类型对象的 tp_hash 函数指针,调用对应的哈希值计算函数 (第 6 行)。 换句话讲, PyObject_Hash 根据对象的类型,调用不同的函数版本。 这不就是 多态 吗? 通过 ob_type 字段, Python 在 C 语言层面实现了对象的 多态 特性, 思路跟 C++ 中的 虚表指针 有异曲同工之妙。

对象的行为#

不同对象的行为不同,比如哈希值计算方法就不同,由类型对象中 tp_hash 字段决定。 除了 tp_hash ,我们看到 PyTypeObject 结构体还定义了很多函数指针,这些指针最终都会指向某个函数,或者为空。 这些函数指针可以看做是 类型对象(PyTypeObject) 中定义的 操作 ,这些操作决定对应 实例对象 在运行时的 行为 。 尽管如此,不同对象也有一些共性。 举个例子,整数对象 和 浮点对象 都支持加减乘除等 数值型操作 :

1
2
3
4
5
>>> 1 + 2
3

>>> 3.14 * 3.14
9.8596
元组对象 tuple 和 列表对象 list 都支持下标操作:
1
2
3
4
5
6
7
>>> t = ('apple', 'banana', 'car', 'dog')
>>> t[-1]
'dog'

>>> l = ['alpha', 'beta']
>>> l[-1]
'beta'
因此,以对象行为为依据,可以 对对象进行分类


Python 便以此为依据,为每个类别都定义了一个 标准操作集 : - PyNumberMethods 结构体定义了 数值型 操作; - PySequenceMethods 结构体定义了 序列型 操作; - PyMappingMethods 结构体定义了 关联型 操作;

只要 类型对象 提供相关 操作集 , 实例对象 便具备对应的 行为 。 操作集字段如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef struct _typeobject {
PyObject_VAR_HEAD
const char *tp_name; /* For printing, in format "<module>.<name>" */
Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */

// ...
/* Method suites for standard classes */

PyNumberMethods *tp_as_number;
PySequenceMethods *tp_as_sequence;
PyMappingMethods *tp_as_mapping;

// ...
/* Functions to access object as input/output buffer */
PyBufferProcs *tp_as_buffer;

// ...
} PyTypeObject;
以 float 为例,类型对象 PyFloat_Type 相关字段是这样初始化的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static PyNumberMethods float_as_number = {
float_add, /* nb_add */
float_sub, /* nb_subtract */
float_mul, /* nb_multiply */
float_rem, /* nb_remainder */
float_divmod, /* nb_divmod */
float_pow, /* nb_power */
// ...
};

//PyFloat_Type是PyTypeObject结构体类型,只是重写了
//注意与c++父类区分开,这里只是重定义了一些具体参数
PyTypeObject PyFloat_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"float",
sizeof(PyFloatObject),

// ...
&float_as_number, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */

// ...
};

  • 字段 tp_as_number 非空,因此 float 对象 支持数值型操作
  • 字段 tp_as_sequence 为空,因此 float 对象 不支持序列型操作
  • 字段 tp_as_mapping 为空,因此 float 对象 不支持关联型操作 ; 注意到, float_as_number 变量中相关函数指针都初始化为对应的 float 版本操作函数。

引用计数#

C/C++ 赋予程序员极大的自由,可以任意申请内存,并按自己的意图灵活管理。 然而,权利的另一面则对应着 责任 ,一旦内存不再使用,程序员必须将其释放。 这给程序员带来极大的 工作负担 ,并导致大量问题: 内存泄露 、 野指针 、 越界访问 等。 许多后来兴起的开发语言,如 Java 、 Golang 等,选择 由语言本身负责内存的管理 。 垃圾回收机制 的引入,程序员摆脱了内存管理的噩梦,可以更专注于业务逻辑。 于此同时,开发人员失去了灵活使用内存的机会,也牺牲了一定的执行效率。 随着垃圾回收机制日益完善,可在大部分对性能要求不苛刻的场景中引入,利大于弊。 Python 也采用垃圾回收机制,代替程序员进行繁重的内存管理,提升开发效率 的同时,降低 bug 发生的几率。 Python 垃圾回收机制的关键是对象的 引用计数 ,它决定了一个对象的生死。 我们知道每个 Python 对象都有一个 ob_refcnt 字段,记录着对象当前的引用计数。 当对象被其他地方引用时, ob_refcnt 加一; 当引用解除时, ob_refcnt 减一。 当 ob_refcnt 为零,说明对象已经没有任何引用了,这时便可将其回收。 Python 对象创建后,引用计数设为 1 :

1
2
3
>>> a = 3.14
>>> sys.getrefcount(a)
2
这里引用计数为啥是 2 呢? 对象 作为函数参数传递,需要将引用计数加一(重),避免对象被提前销毁;函数返回时,再将引用计数减一。 因此,例子中 getrefcount 函数看到的对象引用计数为 2 。 接着,变量赋值让对象多了一个引用,这很好理解:
1
2
3
>>> b = a
>>> sys.getrefcount(a)
3
将对象放在容器对象中,引用计数也增加了,符合预期:
1
2
3
4
5
>>> l = [a]
>>> l
[3.14]
>>> sys.getrefcount(a)
4
我们将 b 变量删除,引用计数减少了:
1
2
3
>>> del b
>>> sys.getrefcount(a)
3
接着,将列表清空,引用计数进一步下降:
1
2
3
>>> l.clear()
>>> sys.getrefcount(a)
2
最后,将变量 a 删除后,引用计数降为 0 ,便不复存在了:
1
>>> del a
在 Python 中,很多场景都涉及引用计数的调整,例如: - 容器操作; - 变量赋值; - 函数参数传递; - 属性操作

为此, Python 定义了两个非常重要的宏,用于维护对象应用计数。 其中, Py_INCREF 将对象应用计数加一 ( 3 行):

1
2
3
#define Py_INCREF(op) (                         \
_Py_INC_REFTOTAL _Py_REF_DEBUG_COMMA \
((PyObject *)(op))->ob_refcnt++)
Py_DECREF 将引用计数减一 ( 5 行),并在引用计数为 0 是回收对象 ( 8 行):
1
2
3
4
5
6
7
8
9
10
#define Py_DECREF(op)                                   \
do { \
PyObject *_py_decref_tmp = (PyObject *)(op); \
if (_Py_DEC_REFTOTAL _Py_REF_DEBUG_COMMA \
--(_py_decref_tmp)->ob_refcnt != 0) \
_Py_CHECK_REFCNT(_py_decref_tmp) \
else
\\ 调用对象对应的析构函数销毁对象
_Py_Dealloc(_py_decref_tmp); \
} while (0)
当一个对象引用计数为 0 , Python 便调用对象对应的析构函数销毁对象,但这并不意味着对象内存一定会回收。 为了提高内存分配效率, Python 为一些 常用对象维护了内存池对象回收后内存进入内存池中,以便下次使用,由此 避免频繁申请、释放内存 。 内存池 技术作为程序开发的高级话题,需要更大的篇幅,放在后续章节中介绍。

面向对象理论中 “ 类 ”和“ 对象 ” 这两个重要概念,在 Python 内部均 以对象的形式存在“类”是一种对象,称为 类型对象 ;“类”实例化生成的“对象”也是对象,称为 实例对象

根据对象不同特点还可进一步分类:

类别 特点
可变对象 对象创建后可以修改
不可变对象 对象创建后不能修改
定长对象 对象大小固定
变长对象 对象大小不固定

那么,对象在 Python 内部到底长啥样呢? 由于 Python 是由 C 语言实现的,因此 Python 对象在 C 语言层面应该是一个 结构体 ,组织对象占用的内存。 不同类型的对象,数据及行为均可能不同,因此可以大胆猜测:不同类型的对象由不同的结构体表示。 对象也有一些共性,比如每个对象都需要有一个 引用计数 ,用于实现 垃圾回收机制 。 因此,还可以进一步猜测:表示对象的结构体有一个 公共头部

PyObject,对象的基石#

在 Python 内部,对象都由 PyObject 结构体表示, 对象引用则是指针 PyObject * 。 PyObject 结构体定义于头文件 object.h ,路径为 Include/object.h ,代码如下:

1
2
3
4
5
typedef struct _object {
_PyObject_HEAD_EXTRA //公共头部
Py_ssize_t ob_refcnt; //引用计数
struct _typeobject *ob_type; //类型指针
} PyObject;
除了 _PyObject_HEAD_EXTRA 宏,结构体包含以下两个字段: - 引用计数 ( ob_refcnt ) - 类型指针 ( ob_type )

引用计数 很好理解:对象被其他地方引用时加一,引用解除时减一; 当引用计数为零,便可将对象回收,这是最简单的垃圾回收机制。 类型指针 指向对象的 类型对象 ,类型对象 描述 实例对象 的数据及行为。

回过头来看 _PyObject_HEAD_EXTRA 宏的定义,同样在 Include/object.h 头文件内:

1
2
3
4
5
6
7
8
9
10
11
12
#ifdef Py_TRACE_REFS
/* Define pointers to support a doubly-linked list of all live heap objects. */
#define _PyObject_HEAD_EXTRA \
struct _object *_ob_next; \
struct _object *_ob_prev;

#define _PyObject_EXTRA_INIT 0, 0,

#else
#define _PyObject_HEAD_EXTRA
#define _PyObject_EXTRA_INIT
#endif

如果 Py_TRACE_REFS 有定义,宏展开为两个指针,看名字是用来实现 双向链表 的:

1
2
struct _object *_ob_next;
struct _object *_ob_prev;
结合注释,双向链表用于 跟踪所有 活跃堆对象 ,一般不启用,不深入介绍。 对于 变长对象 ,需要在 PyObject 基础上加入长度信息,这就是 PyVarObject
1
2
3
4
typedef struct {
PyObject ob_base;
Py_ssize_t ob_size; /* Number of items in variable part */
} PyVarObject;
变长对象比普通对象多一个字段 ob_size ,用于记录元素个数

于具体对象,视其大小是否固定,需要包含头部 PyObject 或 PyVarObject 。 为此,头文件准备了两个宏定义,方便其他对象使用:
1
2
#define PyObject_HEAD          PyObject ob_base;
#define PyObject_VAR_HEAD PyVarObject ob_base;
例如,对于大小固定的 浮点对象 ,只需在 PyObject 头部基础上, 用一个 双精度浮点数 double 加以实现:
1
2
3
4
5
typedef struct {
PyObject_HEAD //浮点数是定长对象

double ob_fval;
} PyFloatObject;
而对于大小不固定的 列表对象 ,则需要在 PyVarObject 头部基础上, 用一个动态数组加以实现,数组存储列表包含的对象,即 PyObject 指针:

如图, PyListObject 底层 由一个数组实现,关键字段是以下 3 个: ob_item ,指向 动态数组 的指针,数组保存元素对象指针; allocated ,动态数组总长度,即列表当前的 容量 ; ob_size ,当前元素个数,即列表当前的 长度 ; 列表容量不足时,Python 会自动扩容,具体做法在讲解 list 源码时再详细介绍。 最后,介绍两个用于初始化对象头部的宏定义。 其中,PyObject_HEAD_INIT 一般用于 定长对象 ,将引用计数 ob_refcnt 设置为 1 并将对象类型 ob_type 设置成给定类型

1
2
3
#define PyObject_HEAD_INIT(type)        \
{ _PyObject_EXTRA_INIT \
1, type },
PyVarObject_HEAD_INIT 在 PyObject_HEAD_INIT 基础上进一步设置 长度字段 ob_size ,一般用于 变长对象 :
1
2
#define PyVarObject_HEAD_INIT(type, size)       \
{ PyObject_HEAD_INIT(type) size },
后续在研读源码过程中,将经常见到这两个宏定义。

PyTypeObject,类型的基石#

在 PyObject 结构体,我们看到了 Python 中所有对象共有的信息。 对于内存中的任一个对象,不管是何类型,它刚开始几个字段肯定符合我们的预期: 引用计数 、 类型指针 以及变长对象特有的 元素个数 。 随着研究不断深入,我们发现有一些棘手的问题没法回答: 不同类型的对象所需内存空间不同,创建对象时从哪得知内存信息呢? 对于给定对象,怎么判断它支持什么操作呢? 对于我们初步解读过的 PyFloatObject 和 PyListObject ,并不包括这些信息。 事实上,这些作为对象的 元信息 ,应该由一个独立实体保存,与对象所属 类型 密切相关。 注意到, PyObject 中包含一个指针 ob_type ,指向一个 类型对象 ,秘密就藏在这里。类型对象 PyTypeObject 也在 Include/object.h 中定义,字段较多,只讨论关键部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef struct _typeobject {
PyObject_VAR_HEAD
const char *tp_name; /* For printing, in format "<module>.<name>" */
Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */

/* Methods to implement standard operations */
destructor tp_dealloc;
printfunc tp_print;

getattrfunc tp_getattr;
setattrfunc tp_setattr;

// ...
/* Attribute descriptor and subclassing stuff */
struct _typeobject *tp_base;

// ......
} PyTypeObject;
可见 类型对象 PyTypeObject 是一个 变长对象 ,包含变长对象头部。 专有字段有: - 类型名称 ,即 tp_name 字段; - 类型的继承信息,例如 tp_base 字段指向基类对象; - 创建实例对象时所需的 内存信息 ,即 tp_basicsize 和 tp_itemsize 字段; - 该类型支持的相关 操作信息 ,即 tp_print 、 tp_getattr 等函数指针;

PyTypeObject 就是 类型对象 在 Python 中的表现形式,对应着面向对象中“类”的概念。 PyTypeObject 结构很复杂,但是我们不必在此刻完全弄懂它。 先有个大概的印象,知道 PyTypeObject 保存着对象的 元信息 ,描述对象的 类型 即可。 接下来,以 浮点 为例,考察 类型对象 和 实例对象 在内存中的形态和关系:

1
2
3
4
5
6
7
>>> float
<class 'float'>

>>> pi = 3.14
>>> e = 2.71
>>> type(pi) is float
True
float 为 浮点类型对象,系统中只有唯一一个,保存了所有浮点实例对象的元信息。 而浮点实例对象就有很多了,圆周率 pi 是一个,自然对数 e 是另一个,当然还有其他。 代码中各个对象在内存的形式如下图所示:

其中,两个浮点 实例对象 都是 PyFloatObject 结构体, 除了公共头部字段 ob_refcnt 和 ob_type ,专有字段 ob_fval 保存了对应的数值。 浮点 类型对象 是一个 PyTypeObject 结构体, 保存了类型名、内存分配信息以及浮点相关操作。 实例对象 ob_type 字段指向类型对象, Python 据此判断对象类型, 进而获悉关于对象的元信息,如操作方法等。 再次提一遍,float 、 pi 以及 e 等变量只是一个指向实际对象的指针。 由于浮点 类型对象 全局唯一,在 C 语言层面 作为一个全局变量静态定义即可,Python 的确就这么做。 浮点类型对象就藏身于 Object/floatobject.c 中, PyFloat_Type 是也:

1
2
3
4
5
6
7
8
9
10
11
12
PyTypeObject PyFloat_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"float",
sizeof(PyFloatObject),
0,
(destructor)float_dealloc, /* tp_dealloc */

// ...
(reprfunc)float_repr, /* tp_repr */

// ...
};
其中,第 2 行初始化 ob_refcnt 、 ob_type 以及 ob_size 三个字段; 第 3 行将 tp_name 字段初始化成类型名称 float ;再往下是各种操作的函数指针。 注意到 ob_type 指针指向 PyType_Type ,这也是一个静态定义的全局变量。 由此可见,代表“ 类型的类型 ” 即 type 的那个对象应该就是 PyType_Type 了。

PyType_Type,类型的类型#

我们初步考察了 float 类型对象,知道它在 C 语言层面是 PyFloat_Type 全局静态变量。 类型是一种对象,它也有自己的类型,也就是 Python 中的 type :

1
2
>>> float.__class__
<class 'type'>
自定义类型也是如此:
1
2
3
4
5
>>> class Foo(object):
... pass
...
>>> Foo.__class__
<class 'type'>
那么, type 在 C 语言层面又长啥样呢? 围观 PyFloat_Type 时,我们通过 ob_type 字段揪住了 PyType_Type 。 的确,它就是 type 的肉身。 PyType_Type 在 Object/typeobject.c 中定义(也是 重写PyTypeObject结构体):
1
2
3
4
5
6
7
8
9
10
11
12
PyTypeObject PyType_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"type", /* tp_name */
sizeof(PyHeapTypeObject), /* tp_basicsize */
sizeof(PyMemberDef), /* tp_itemsize */
(destructor)type_dealloc, /* tp_dealloc */

// ...
(reprfunc)type_repr, /* tp_repr */

// ...
};
内建类型和自定义类对应的 PyTypeObject 对象都是这个通过 PyType_Type 创建的。 PyType_Type 在 Python 的类型机制中是一个至关重要的对象,它是所有类型的类型,称为 元类型 ( meta class )。 借助元类型,你可以实现很多神奇的高级操作。 注意到, PyType_Type 将自己的 ob_type 字段设置成它自己(第 2 行),这跟我们在 Python 中看到的行为是吻合的:
1
2
3
4
5
>>> type.__class__
<class 'type'>

>>> type.__class__ is type
True
至此,元类型 type 在对象体系里的位置非常清晰了:

PyBaseObject_Type,类型之基#

object 是另一个特殊的类型,它是 所有类型的基类。 那么,怎么找到它背后的实体呢? 理论上,通过 PyFloat_Type 中 tp_base 字段顺藤摸瓜即可。 然而,我们发现这个字段在并没有初始化:

1
0,                                          /* tp_base */
这又是什么鬼? 接着查找代码中 PyFloat_Type 出现的地方,我们在 Object/object.c 发现了蛛丝马迹:
1
2
if (PyType_Ready(&PyFloat_Type) < 0)
Py_FatalError("Can't initialize float type");
敢情 PyFloat_Type 静态定义后还是个半成品呀! PyType_Ready 对它做进一步加工,将 PyFloat_Type 中 tp_base 字段初始化成 PyBaseObject_Type :
1
2
3
4
5
6
7
8
9
10
11
12
13
int
PyType_Ready(PyTypeObject *type)
{
// ...

base = type->tp_base;
if (base == NULL && type != &PyBaseObject_Type) {
base = type->tp_base = &PyBaseObject_Type;
Py_INCREF(base);
}

// ...
}
PyBaseObject_Type 就是 PyTypeObject 背后的实体:

1
2
3
4
5
6
7
8
9
10
PyTypeObject PyBaseObject_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"object", /* tp_name */
sizeof(PyObject), /* tp_basicsize */
0, /* tp_itemsize */
object_dealloc, /* tp_dealloc */

// ...
object_repr, /* tp_repr */
};

注意到, ob_type 字段指向 PyType_Type 跟 object 在 Python 中的行为时相吻合的:

1
2
>>> object.__class__
<class 'type'>
又注意到 PyType_Ready 函数初始化 PyBaseObject_Type 时,不设置 tp_base 字段。 因为继承链必须有一个终点,不然对象沿着继承链进行属性查找时便陷入死循环。
1
2
>>> print(object.__base__)
None
至此,我们完全弄清了 Python 对象体系中的所有实体以及关系,得到一幅完整的图画:

  1. 创建一个对象时, 先创建一个 类型对象PyTypeObject(类型对象自始至终都是 只要一个的, 在C源码中, 就是定义了一个全局的变量), 保存要创建对象的类型信息, 接着再在 该类型对象的方法中创建指定的对象, 并将类型对象 PyTypeObject 置为该对象的属性之一, 如创建一个int对象, 先在PyInt_Type对象创建封装了信息之后再创建PyIntObject对象。

  2. Python相比较于其他语言的好处是 其doc文档就在程序之中, 通过PyTypeObject结构体中的doc属性可以看到。

GPU渲染管线#

当GPU从CPU得到渲染命令后,就会进行一系列流水线操作,最终把图元渲染到屏幕上。

对于几何阶段和光栅化阶段,开发者无法拥有绝对的控制权,其实现的载体是GPU。GPU通过实现流水线化,大大加快了渲染速度

几何阶段和光栅化阶段可以分成若干更小的流水线阶段,这些流水线阶段由GPU实现,每个阶段GPU提供了不同的可配置性或可编程性。

从图中可以看出,GPU的渲染流水线接收顶点数据作为输入。这些顶点数据是由应用阶段加载到显存中,再由DrawCall指定的。这些数据随后被传递给顶点着色器。

1.顶点着色器(Vertex Shader):完全可编程#

通常用于实现顶点的空间变换、顶点着色等功能。

顶点着色器是流水线的第一个阶段,它的输入来自于CPU。顶点着色器的处理单位是顶点,输入进来的每个顶点都会调用一次顶点着色器。顶点着色器本身并不会创建或者销毁顶点,而且无法得到顶点与顶点之间的关系。

例如:我们无法得知两个顶点是否属于一个三角网格。正因为这样的相互独立性,GPU可以利用本身的特性并行化处理每一个顶点。

顶点着色器需要完成的工作主要有:坐标变换和顶点光照。当然还可以输出后续阶段所需的数据。

2.裁剪(Clipping):可配置#

这一阶段的目的是将那些不再摄像机视野内的顶点裁减掉,并剔除某些三角图元的面片。

目的:处理不在摄像机视野范围内的物体。

一个图元和摄像机视野有三种关系:完全在视野内、部分在视野内、完全在视野外。完全在视野内的图元即系传递给下一个流水线阶段,完全在视野外的图元就不会向下传递,因为不需要被渲染。而那些部分在视野内的图元就需要进行一个处理,那就是裁剪。

和顶点着色器不同,这一步是不可编程的。无法通过编程来控制裁剪的过程,是硬件上的固定操作

但是可以自定义一个剪裁操作来对这一步进行配置

3.屏幕映射(Screen Mapping):不可配置和编程的#

这一步输入的坐标仍然是三维坐标系下的坐标。

负责把每个图元的坐标转换到屏幕坐标系中。

4.三角形设置(Triangle Setup)和三角形变量(Triangle Traversal)阶段都是固定函数(Fixed-Function)的阶段#

这一阶段开始进入光栅化阶段。从上一个阶段输出的信息是屏幕坐标系下的顶点位置以及和它们相关的额外信息,如:深度值(Z)、法线方向、视角方向等。

光栅化阶段有两个重要的目标:计算每个图元的覆盖了哪些像素,以及为这些像素计算颜色。

三角形设置:计算光栅化一个三角网格所需的信息。具体来讲,上一个阶段输出的都是三角网格的顶点,即我们得到的是三角网格每条边的两个顶点。但是如果要得到整个三角形网格对像素的覆盖情况,我们就必须计算每条边上的像素坐标。为了能够计算边界像素的坐标信息,我们就需要得到三角形边界的表示方式。这样一个计算三角形网格表示数据的过程叫做三角形设置。

三角形遍历:检查每个像素是否被一个三角网格所覆盖。如果覆盖的话,就会生成一个片段(fragment)。这样一个找到哪些像素被三角网格覆盖的过程就是三角形遍历,这个阶段也被称为扫描变换(Scan Conversion)。

片段并不是真正意义上的像素,而是包含了很多状态的集合,这些状态用于计算每个像素的最终颜色。

5.片段着色器(Fragment Shader):完全可编程#

用于实现逐片元(Per-Fragment)的着色操作。

片段着色器的输入是上一个阶段对顶点信息插值得到的结果,更具体来说,是根据那些从顶点着色器中输出的数据插值得到的。输出的是一个或者多个颜色值。

这一个阶段可以完成很多重要的渲染技术,其中最重要的技术之一就是纹理采样。为了在片段着色器中进行纹理采样,通常会在顶点着色器阶段输出每个顶点对应的纹理坐标,然后经过光栅化阶段对三角网格的3个顶点对应的纹理进行插值后,就可以得到其覆盖的片段的纹理坐标了。

6.逐片元操作(Per-Fragment Operations):不可编程,但具有很高的可配置性#

负责执行很多重要的操作,例如:修改颜色、深度缓冲、进行混合等。

这一阶段OpenGL-逐片元操作 DirectX-输出合并阶段

这一阶段有几个重要的任务:

(1) 决定每个片元的可见性:这涉及了很多测试工作,例如:深度测试、模板测试

(2) 如果一个片元通过了所有的测试,就需要把这个片元的颜色值和已经储存在颜色缓冲区中的颜色进行合并,混合

1. 概述#

1.1 shader种类#

shader主要有:固定管线着色器,顶点片元着色器,表面着色器

  • 固定管线着色器(逐渐淘汰)
  • 顶点shader:干预模型形态的shader(顶点变换)
  • 像素shader:干预像素着色的shader(不同纹理贴图)

1.2 GPU编程语言#

  1. Dirext3D-----微软(windows)
  2. OpenGL-----Linux(Android)

目前面向GPU的编程有三种高级图像语言:HLSL语言,GLSL语言,Cg语言

  1. HLSL语言:High Level Shading Language,由Microsoft公司提供,通过Direct3D图形软件库来编写的着色器语言。

  2. GLSL语言:OpenGL Shading Language,由OpenGL安委会提供,在OpenGL中进行着色器编程的语言。

  3. Cg语言:C for Graphics,由NVIDIA公司和Microsoft公司合作提供,有自己的一套关键字和函数库,独立于三维编程接口,在Direct3D和OpenGL上都可工作

2. shader lab基础语法#

2.1 ShaderLab简介#

ShaderLab: Unity 自己又封装了一层CG/HLSL/GLSL的接口,但为了实现跨平台,Unity重点支持Cg着色器语言

1
2
3
4
5
6
7
8

Shader "主色器名称"{
Properties{} //属性定义
SubShader{} //子主色器
SubShader{} //子主色器2
…………
FallBack "备用着色器名称" //如果所有子着色器不能运行,则使用备用着色器
}

简单的shader示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// colored vertex lighting
Shader "Simple colored lighting"
{
// a single color property
Properties {
_Color ("Main Color", Color) = (1,.5,.5,1)
}
// define one subshader
SubShader
{
// a single pass in our subshader
Pass
{
// use fixed function per-vertex lighting
Material
{
Diffuse [_Color]
}
Lighting On
}
}
}
Shader "name"{ }定义了一个名字为“name”的shader。

2.1.1 properties属性定义#

用来定义主色器中使用的贴图资源或者数值参数等。这些属性会在inspector视图的材质界面中显示,可以方便的进行设置和修改。

2.1.2 SubShader属性定义#

一个着色器包含一个或者多个子着色器。当Unity使用着色器渲染的时,会从上到下遍历子着色器,找到第一个被用户设备支持的子着色器,并使用该子着色器进行渲染。如果没有子着色器可以使用,则使用备用着色器

2.1.3 Fallback属性定义#

备用着色器一般会指定一个对硬件要求最低的shader。当所有子着色器不能运行的时候,unity会启用备用着色器来进行渲染。

2.2 Properties属性#

properties一般定义在着色器的起始部分,我们可以在Shader书写的时候定义多种多样的属性,而使用Shader的时候可以直接在材质检视面板(Material Inspector)里编辑这些属性,取不同的值或者纹理。这可以说是Unity贴心&可见即所得的又一体现吧。

定义了一些属性参数,可在Unity编辑器的“Inspector”面板中编辑和调整。

1
2
3
4
5
6
_Name("Display Name", type) = defaultValue[{options}]


Properties {
_Color是变量名 ("Main Color"是在“Inspector”中的名字, Color是变量类型) = (1,.5,.5,1)是默认值          
}

2.2.1 Properties 类型#

类型|说明 -:-|-:- Range(min,max)| 在(min,max)范围内的浮点数 Float |浮点数 Int |整型 Color |颜色 RGBA Vector |四维向量 2D |2D纹理 3D |3D纹理 Cube |立方体贴图纹理 Rect |矩形纹理

  • **_Name** 属性的名字,简单说就是变量名,在之后整个Shader代码中将使用这个名字来获取该属性的内容。Unity中用下划线开始_Name
  • Display Name - 这个字符串将显示在Unity的材质编辑器中作为Shader的使用者可读的内容
  • type - 这个属性的类型,可能的type所表示的内容有以下几种:
    • Color - 一种颜色,由RGBA(红绿蓝和透明度)四个量来定义;
    • 2D - 一张2的阶数大小(256,512之类)的贴图。这张贴图将在采样后被转为对应基于模型UV的每个像素的颜色,最终被显示出来;
    • Rect - 一个非2阶数大小的贴图;
    • Cube - 即Cube map texture(立方体纹理),简单说就是6张有联系的2D贴图的组合,主要用来做反射效果(比如天空盒和动态反射),也会被转换为对应点的采样;
    • Range(min, max) - 一个介于最小值和最大值之间的浮点数,一般用来当作调整Shader某些特性的参数(比如透明度渲染的截止值可以是从0至1的值等);
    • Float - 任意一个浮点数;
    • Vector - 一个四维数;
  • defaultValue 定义了这个属性的默认值,通过输入一个符合格式的默认值来指定对应属性的初始值(某些效果可能需要某些特定的参数值来达到需要的效果,虽然这些值可以在之后在进行调整,但是如果默认就指定为想要的值的话就省去了一个个调整的时间,方便很多)。
    • Color - 以0~1定义的rgba颜色,比如(1,1,1,1);
    • 2D/Rect/Cube - 对于贴图来说,默认值可以为一个代表默认tint颜色的字符串,可以是空字符串或者”white”,”black”,”gray”,”bump”中的一个
    • Float,Range - 某个指定的浮点数
    • Vector - 一个4维数,写为 (x,y,z,w)
  • 另外还有一个{option},它只对2D,Rect或者Cube贴图有关,在写输入时我们最少要在贴图之后写一对什么都不含的空白的{},当我们需要打开特定选项时可以把其写在这对花括号内。如果需要同时打开多个选项,可以使用空白分隔。可能的选择有ObjectLinear, EyeLinear, SphereMap, CubeReflect, CubeNormal中的一个,这些都是OpenGL中TexGen的模式,

2.3 SubShader#

2.3.1 定义语法#

1
2
3
4
Subshader { 
[Tags]
[CommonState]
Passdef [Passdef ...] }

2.3.2 标签(Tags)#

1
2
3
4
//写在SubShader的第一句
Tags { "TagName1" = "Value1" "TagName2" = "Value2" }
//比如
Tags { "RenderType"="Opaque" }

作用#

SubShaders使用标签来告诉引擎如何以及何时将其渲染。

Unity识别的以下标记必须在SubShader节内,而不是Pass!

  1. “Queue”标签。定义渲染顺序-队列标签。预制的值为 (1)”Background”。值为1000。比如用于天空盒。 (2)”Geometry”。值为2000。大部分物体在这个队列。不透明的物体也在这里。这个队列内部的物体的渲染顺序会有进一步的优化(应该是从近到远,early-z test可以剔除不需经过FS处理的片元)。其他队列的物体都是按空间位置的从远到近进行渲染。 (3)”AlphaTest”。值为2450。已进行AlphaTest的物体在这个队列。 (4)”Transparent”。值为3000。透明物体。 (5)”Overlay”。值为4000。比如镜头光晕。 (6)用户可以定义任意值,比如”Queue”=”Geometry+10”
  2. “RenderType”标签。Unity可以运行时替换符合特定RenderType的所有ShaderCamera.RenderWithShader或者Camera.SetReplacementShader配合使用。Unity内置的RenderType包括: (1)”Opaque”:绝大部分不透明的物体都使用这个; (2)”Transparent”:绝大部分透明的物体、包括粒子特效都使用这个; (3)”Background”:天空盒都使用这个; (4)”Overlay”:GUI、镜头光晕都使用这个; (5)还有其他可参考Rendering with Replaced Shaders;用户也可以定义任意自己的RenderType字符串。
  3. ”ForceNoShadowCasting”,值为”true”时,表示不接受阴影
  4. ”IgnoreProjector”,值为”true”时,表示不接受Projector组件的投影

2.3.3 Pass(通道:插入到渲染流水线)的定义语法#

subshader包装了一个渲染方案,这些方案由一个个通道(pass)来执行的,subshader可以包含很多通道块,每个Pass都能使几何体渲染一次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Pass{
[Name and Tags名称和标签]
[Render Setup 渲染设置]
[Texture setup 纹理设置]
}

//实例代码
Shader "Custom/Shader_01" {
SubShader {
Pass{
Name "ONE"
Material{
Diffuse(1,0.7,0.4,1)
Ambient(1,0.7,0.4,1)
}
Lighting On
SetTexture[_]{ combine primary }

}
}
}

2.3.4 pass名称和标签(Name and tags)#

可以定义Pass的名字以及任意数量的标签。为Pass命名后,可以在别的着色器中通过Pass名称来引用它,减少重复操作。Name命名必须大写

2.3.5 pass渲染设置(Render Setup)#

pass里可以设置图形硬件的各种状态,例如开启Alpha混合,开启雾效等。

RegularPass渲染设置#

命令 |说明 -:-|-:- Meterial{} |材质,定义一个使用顶点光照管线的材质 Lighting |光照,设置光照,取值为off或on Cull |裁剪,设置裁剪模式,模式包括:back、Front、off ZTest |深度测试,设置深度测试,包括:Less、Greater、LEqual、GEqual、Equal、NotEqual、Always。 ZWrite |深度缓存写入,设置深度缓存写入的开关,取值为Off或On Fog{} |雾效,设置雾效参数 AlphaTest |Alpha测试,设置Alpha测试,包括:Less、Greater、LEqual、GEqual、Equal、NotEqual、Always。 Blend |Alpha混合,设置Alpha混合模式 Stencil| 蒙版,用蒙版来实现像素的取舍操作,选项有:keep、Zero、Replace、Incrsat、DecrSat、Invert、Incrwrap和DecrWrap Color |颜色,是指顶点光照关闭时使用的颜色值 ColorMask |颜色遮罩,设置颜色遮罩,当值为0时关闭所有的颜色通道的渲染,取值为RGB offset |深度偏移,设置深度偏移 SeparateSpecular |高光颜色。开启或关闭顶点光照的独立高光颜色,取值为On或Off ColorMaterial |颜色集,当计算顶点光照时使用每个顶点的颜色。

特殊通道#

  1. UsePass 命令 使用 来自另一个着色器的命名通道

    1
    **UsePass "Shader/Name"**
    插入所有来自给定着色器中的给定名字的通道。Shader/Name包含了通过斜杠字符分割的着色器的名字和通道的名字 某些着色器重用其他着色器中已存在的通道,减少重复的代码。例如,在许多像素光照着色器中,阴影色或顶点光照通道在在相应的顶点光照着色器中是相同的。UsePass命令只是包含了另一个着色器的给定通道。例如当如下的命令可以使用内置的高光着色器中的名叫"Base"的通道:
    1
    UsePass "Specular/BASE"
    为了让UsePass能正常工作,必须给希望使用的通道命名。通道中的Name命令将提供这个名字:
    1
    Name "MyPassName"
    注意,所有通道名字都是大写开头,因此UsePass必须使用大写开头的名字来书写索引

  2. GrabPass是一种特殊的通道类型 - 捕获物体所在位置的屏幕的内容并写入到一个纹理中。这个纹理能被用于后续的通道中完成一些高级图像特效。 有两种方式将GrabPass放入一个 subshader中:

  • GrabPass {} 能捕获当前屏幕的内容到一个纹理中。纹理能在后续通道中通过 _GrabTexture 进行访问。注意:这种形式的捕获通道将在每一个使用该通道的对象渲染过程中执行昂贵的屏幕捕获操作
  • GrabPass { "TextureName" } 能捕获屏幕内容到一个纹理中,但只会在每帧中处理第一个使用给定纹理名的纹理的对象的渲染过程中产生捕获操作。纹理在未来的通道中可以通过给定的纹理名访问。当你在一个场景中拥有多个使用GrabPass的对象时将提高性能。

GrabPass能使用Name 和 Tags命令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Shader "GrabPassInvert" {
SubShader {
// Draw ourselves after all opaque geometry
// 在所有不透明几何体之后自画
Tags { "Queue" = "Transparent" }

// Grab the screen behind the object into _GrabTexture
// 捕获对象后的屏幕到_GrabTexture
GrabPass { }

// Render the object with the texture generated above, and invert it's colors
// 用前面捕获的纹理渲染对象,并反相它的颜色
Pass {
SetTexture [_GrabTexture] { combine one-texture }
}
}
}

2.3.6 pass纹理设置(Texture Setup)#

纹理设置语法:setTexture 纹理属性{[命令选项} 纹理设置用于固定功能管线,如果使用表面着色器或者自定义的顶点或者片段着色器,那么纹理设置将会被忽略。 setTexture的命令选项包括三种: 1. Conbine:将两个颜色源混合,混合源可以是previous(上一次setTexture的结果)、constant(常量颜色值)、primary(顶点颜色)和texture(纹理颜色中的一种) 2. ConstantColor设置一个颜色常量 3. matrix:设置矩阵对纹理坐标进行变换

2.4 Fallback备用着色器#

语法:#

1
2
(1)Fallback “备用着色器名称”
(2)Fallback off

2.5 unity内置Surface shader关照效果计算的性能开销排序#

  1. Unlit:仅适用纹理颜色,不受光照影响
  2. VertexLit:顶点光照
  3. Diffuse:漫反射
  4. Specular:在满反射的基础上增加了高光计算
  5. Normal Mapped:法线贴图,增加了一张法线贴图和几个着色器指令
  6. Normal Mapped Specular:带高光的法线贴图
  7. Parallax Normal Mapped:视差法线贴图,增加了视察贴图的计算开销。
  8. Parallax Normal Mapped Specular:带高光的视差法线贴图

线程安全单例最佳实践,C#中的Lazy是如何保证线程安全的#

在.NET 4.0之后,.NET Framework中提供了一种安全的延迟加载类型Lazy。Lazy能够在多线程环境下,保证GetValue函数只执行一次,从而实现单例模式。

在过去,实现单例模式我们通常使用二次判断锁,或者利用类的静态初始化函数利用Lazy类型,能够简化这一过程,并且性能上更好。

Lazy创建的时候可以指定线程安装模式,目前有两种模式,PublicationOnly,ExcutionAndPublication。

延迟初始化,在第一次使用该对象时再对其进行初始化,如果没有用到则不需要进行初始化,这样的话,使用延迟初始化就提高程序的效率,从而使程序占用更少的内存。

#### PublicationOnly模式
1
2
3
4
5
6
7
8
9
10
11
12
boxed = CreateValue(); //1
if (boxed == null ||
//比较m_boxed与null,如果不相等,什么都不做;如果m_boxed与null相等,则用boxed替换m_boxed的值。
//无论比较结果相等与否,返回值都是m_boxed中原有的值。
Interlocked.CompareExchange(ref m_boxed, boxed, null) != null) //2
{
boxed = (Boxed)m_boxed; //3
}
else
{
m_valueFactory = ALREADY_INVOKED_SENTINEL; //4
}
1. 运行初始化函数,装箱到一个内部Box类型中,解决null值判断的问题,如果已经创建的情况,会返回null,该过程是线程不安全的
2. 判断m_boxed是否为空,m_boxed是value保存的字段,如果等于空则设置为boxed,该方法能保证原子性,该过程是线程安全的
3. 如果CreateValue返回空,表示其他线程已经创建有实例,则设置为已经创建好的实例
4. 将初始化方法标记为已经初始化,一般发生在并发运行情况下,多次运行CreateValue
PublicationOnly模式下使用基于Interlocked.CompareExchange(原子性运算)实现的乐观锁,该类包含了原子性方法 CAS(Compare and swap)
CAS是利用CPU提供的原子性指令来实现,不同运行时版本可能有不一样实现。Interlocked具体的实现在Native方法中,有兴趣的朋友可以通过coreclr/jvm代码查看具体实现
这种模式下,单例函数可能多次运行,但是最终能保证获取到的实例只有一个

ExcutionAndPublication模式#

ExcutionAndPublication模式下使用的是Volatile+Monitor,Monitor就是lock语句的实现,Monitor实现在Native代码中,是重量级的锁

Monitor支持队列和线程睡眠,能够保证一整个方法块处于单线程执行状态

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
object threadSafeObj = Volatile.Read(ref m_threadSafeObj); //强制从主内存空间同步变量到线程内存空间副本
bool lockTaken = false;
try
{
if (threadSafeObj != (object)ALREADY_INVOKED_SENTINEL) //此时会有多个线程获取到正确值,抢夺开始
Monitor.Enter(threadSafeObj, ref lockTaken); //尝试等待锁,进入成功设置lockTaken为true
else
Contract.Assert(m_boxed != null);
            //单线程代码块 Start
if (m_boxed == null) //没有设置值的情况
{
boxed = CreateValue(); //获取值
m_boxed = boxed; //设置到字段中
Volatile.Write(ref m_threadSafeObj, ALREADY_INVOKED_SENTINEL); //强制将线程内存空间副本写入到主内存空间
}
else // got the lock but the value is not null anymore, check if it is created by another thread or faulted and throw if so
{
boxed = m_boxed as Boxed;
if (boxed == null) // it is not Boxed, so it is a LazyInternalExceptionHolder
{
LazyInternalExceptionHolder exHolder = m_boxed as LazyInternalExceptionHolder;
Contract.Assert(exHolder != null);
exHolder.m_edi.Throw();
}
}
            //单线程代码块End
}
finally
{
if (lockTaken) //进入成功需要释放,避免死锁
Monitor.Exit(threadSafeObj);
}