Archive for December, 2007

ThreadPool的使用

Thursday, December 27th, 2007

ThreadPool的使用

笔记: gashero

目录

1   简介

Nicky介绍给我使用的,其接口与其他很多线程池包装都差不多,不过因为只有一个模块,比较容易附带在程序中,所以研究下。 python threadpool 据介绍代码来自《Python in a Nutshell》的14.5节。

2   简单的使用

代码中给出的例子:

>>> pool=ThreadPool(poolsize)
>>> requests=makeRequests(some_callable,list_of_args,callback)
>>> [pool.putRequest(req) for req in requests
>>> pool.wait()

可见使用步骤如下:

  1. 建立线程池对象,其实是个线程管理器
  2. 建立计算任务对象Request
  3. 将计算任务对象放入线程池当中
  4. 等待计算完成

3   接口文档

英文见 http://chrisarndt.de/en/software/python/threadpool/api/

makeRequests(callable,args_list,callback=None,exc_callback=None)

创建多个计算请求,并允许有不同的参数。

参数列表中的每一个元素是两个元素的元组,分别是位置参数列表和关键字参数字典。

class ThreadPool

线程池类,发布工作请求并收集结果。

__init__(self,num_workers,q_size)

构造函数,设置线程池工作线程数量和最大任务队列长度。 num_workers 是初始化时的线程数量。如果 q_size>0 则会限制工作队列的长度,并且在工作队列满时阻塞继续插入工作请求的任务。

createWorkers(self,num_workers)

增加工作线程数量。

dismissWorkers(self,num_workers)

减少工作线程数量。

pool(self,block)

处理队列中的新结果。也就是循环的调用各个线程结果中的回调和错误回调。不过,当请求队列为空时会抛出 NoResultPending 异常,以表示所有的结果都处理完了。这个特点对于依赖线程执行结果继续加入请求队列的方式不太适合。

putRequest(self,request,block=True,timeout=0)

加入一个任务请求到工作队列。

wait(self)

等待执行结果,直到所有任务完成。

class WorkerThread

工作者线程,供ThreadPool内部使用,不必关注。其自定义方法也只有一个。

class WorkRequest

任务请求类。

__init__(self,callable,args=None,kwds=None,requestID=None,callback=None,exc_callback=None)

创建一个工作请求。

4   ThreadPool的递归任务管理问题

如果ThreadPool执行的任务中还会添加任务则需要多考虑几个问题。

如果一个这样的任务正在运行,尚未完成时任务列表就已经空了,那么ThreadPool会立即抛出 NoResultsPending 异常,以告知 wait() 方法所有任务都完成了。而事实上,还有一个线程尚未执行完成。

这种情况下,可以自己设置一个退出条件自己重新实现 wait() 方法。在循环中调用 poll(True) 方法。对于抛出的 NoResultsPending 异常视而不见。并自己设置循环的退出方法。

5   回调函数的使用

建立任务请求时有两种回调函数 callback 和 exc_callback ,他们的回调接口为:

callback(request,result)

exc_callback(request,sys.exc_info())

其中 request 为 WorkRequest 对象。而 result 则是调用线程函数正确的返回结果。 sys.exc_info() 为发生异常时返回的信息。 sys.exc_info() 是一个拥有3个元素的元组。分别为:

  • 异常类 :发生异常的类
  • 异常实例 :如上异常类的实例,包含更多详细信息
  • 跟踪信息 :traceback对象,可以显示错误的行号等等具体的错误信息

Warning

注意,如果没有设置 exc_callback 则发生异常时会将异常信息写入 callback 回调函数。如果同时没有设置 callback 和 exc_callback 则发生任何异常都不会有提示,根本无法调试。

5.1   使用 sys.exc_info() 信息

由于发生异常时返回的 sys.exc_info() 内容并不易读,所以可以用如下方式定制错误回调函数,将错误信息打印出来,或者可选的输出到日志文件。

import traceback
def exc_callback(excinfo):
    errorstr=''.join(traceback.format_exception(*excinfo))
    print errorstr

这样的显示结果就如同控制台中看到的错误跟踪一样了。

用Erlang实现的斐波拉契数列计算

Wednesday, December 19th, 2007

用Erlang实现的斐波拉契数列计算

作者: gashero

前几日有好友发来帖子告诉我一个Ruby比Python快的实验,见了以后立马写了一个程序,不过优化效果很有限。最后倒是想起来erlang,一直吵吵着想学,但是一直学的都不紧不慢的,没效果。于是准备拿这个例子来尝试一下。

经过了好久的学习,最终写出了一个比较龌龊的例子,如下:

-module(fibmod).
-export([fib/1,calc/1,print/2]).

fib(0) ->
    0;
fib(1) ->
    1;
fib(X) ->
    fib(X-1)+fib(X-2).

calc(35) ->
    print(35,fib(35));
calc(N) ->
    print(N,fib(N)),
    calc(N+1).

print(Number,Fibnum) ->
    io:format("n=~w => ~w~n",[Number,Fibnum]).

也就是这个例子了,要在erlang控制台中运行,需要按照如下步骤:

10> c(fibmod).
{ok,fibmod}
11> fibmod:calc(1).
n=1 => 1
n=2 => 1
n=3 => 2
n=4 => 3
n=5 => 5
n=6 => 8
n=7 => 13
n=8 => 21
n=9 => 34
n=10 => 55
n=11 => 89
n=12 => 144
n=13 => 233
n=14 => 377
n=15 => 610
n=16 => 987
n=17 => 1597
n=18 => 2584
n=19 => 4181
n=20 => 6765
n=21 => 10946
n=22 => 17711
n=23 => 28657
n=24 => 46368
n=25 => 75025
n=26 => 121393
n=27 => 196418
n=28 => 317811
n=29 => 514229
n=30 => 832040
n=31 => 1346269
n=32 => 2178309
n=33 => 3524578
n=34 => 5702887
n=35 => 9227465
ok
12>

当时按照例子,Ruby1.9要十几秒,Python的要三十几秒。不过我在这台机器上没有测试。我在这台笔记本(T43 1.86GHz)上运行如上erlang程序,估算时间是4秒以内(注:不知道如何用erlang计时)。

其实例子超级简单,不过多少算是第一个erlang程序,希望是一个好的开始。现在越来越看好erlang了,也祝愿erlang发展的越来越好吧。

在应用中嵌入Python

Wednesday, December 19th, 2007

在应用中嵌入Python

翻译: gashero

前面的章节讨论如何扩展Python,如何生成适合的C库等。不过还有另一种情况:通过将Python嵌入C/C++应用以扩展程序的功能。Python嵌入实现了一些使用Python更合适的功能。这可以有很多用途,一个例子是允许用户裁减需要的Python功能。也可以用于默写使用Python编写更加方便的功能。

嵌入Python与扩展很像。扩展Python时,主程序是Python解释器,但是嵌入Python则主程序并不是Python的-是程序的其他部分调用Python来实现一些功能。

所以,如果要嵌入Python,你可以提供自己的主程序,这个主程序需要初始化Python解释器。至少需要调用函数 Py_Initialize() (对于MacOS,调用 PyMac_Initialize())。可以选择是否传入命令行参数到Python。然后你就可以在应用的任何地方调用Python解释器了。

有几种方法调用解释器:可以传递一个包含Python语句的字符串到 PyRun_SimpleString() ,也可以传递一个stdio文件指针和一个文件名(用于识别错误信息)到 PyRun_SimpleFile() 。你也可以调用前几章介绍的底层操作直接控制Python对象。

可以在目录 Demo/embed/ 中找到嵌入Python的例子。

目录

1   高层次嵌入

嵌入Python最简单的形式是使用高层次的接口。这个接口专门用于执行Python脚本,而不需要与应用程序直接交互。例子可以在一个文件中展示:

#include <Python.h>
int
main(int argc, char* argv[]) {
    Py_Initialize();
    PyRun_SimpleString("from time import time,ctime\n"
            "print 'Today is',ctime(time())\n");
    Py_Finalize();
    return 0;
}

如上代码首先使用 Py_Initialize() 初始化Python解释器,随后执行硬编码中的Python脚本来打印日期和时间。最后 Py_Finalize() 关闭了解释器。在真实应用中,你可能希望从其他方式获取Python脚本,文件、编辑器、数据库等。从文件获取的方式更适合使用 PyRun_SimpleFile() 函数,可以省去分配内存空间和载入文件的麻烦。

2   超越高层嵌入:预览

高层次的接口可以方便的执行Python代码,但是交换数据就很麻烦。如果需要,你可以使用低层次的接口调用。虽然多写一些C代码,但是却可以完成很多功能。

仍然要提醒的是,Python的扩展与嵌入其实很像,尽管目的不同。前几章讨论的大多数问题在这里也同样适用。可以参考用C扩展Python时一些步骤:

  1. 转换Python类型到C类型
  2. 传递参数并调用C函数
  3. 转换返回值到Python

当嵌入Python时,接口需要做:

  1. 转换C数据到Python
  2. 调用Python接口程序来调用Python函数
  3. 转化返回值到C

有如你所见,数据转换的步骤用于跨语言的数据交换。唯一的不同是两次数据转换之间调用的函数。当扩展时,你调用C函数,当嵌入时,调用Python函数。

这一章不会讨论Python和C之间的数据转换。并且假设你会使用手册来处理错误,自此只会讨论与扩展解释器不同的部分,你可以到前面的章节找到需要的信息。

3   纯扩展

第一个程序是执行一段Python脚本中的函数。有如高层接口一节,Python解释器并不会自动与程序结合。

运行一段Python脚本中的函数的代码如下:

#include <Python.h>

int
main(int argc, char *argv[])
{
    PyObject *pName, *pModule, *pDict, *pFunc;
    PyObject *pArgs, *pValue;
    int i;

    if (argc < 3) {
        fprintf(stderr,"Usage: call pythonfile funcname [args]\n");
        return 1;
    }

    Py_Initialize();
    pName = PyString_FromString(argv[1]);
    /* Error checking of pName left out */

    pModule = PyImport_Import(pName);
    Py_DECREF(pName);

    if (pModule != NULL) {
        pFunc = PyObject_GetAttrString(pModule, argv[2]);
        /* pFunc is a new reference */

        if (pFunc && PyCallable_Check(pFunc)) {
            pArgs = PyTuple_New(argc - 3);
            for (i = 0; i < argc - 3; ++i) {
                pValue = PyInt_FromLong(atoi(argv[i + 3]));
                if (!pValue) {
                    Py_DECREF(pArgs);
                    Py_DECREF(pModule);
                    fprintf(stderr, "Cannot convert argument\n");
                    return 1;
                }
                /* pValue reference stolen here: */
                PyTuple_SetItem(pArgs, i, pValue);
            }
            pValue = PyObject_CallObject(pFunc, pArgs);
            Py_DECREF(pArgs);
            if (pValue != NULL) {
                printf("Result of call: %ld\n", PyInt_AsLong(pValue));
                Py_DECREF(pValue);
            }
            else {
                Py_DECREF(pFunc);
                Py_DECREF(pModule);
                PyErr_Print();
                fprintf(stderr,"Call failed\n");
                return 1;
            }
        }
        else {
            if (PyErr_Occurred())
                PyErr_Print();
            fprintf(stderr, "Cannot find function \"%s\"\n", argv[2]);
        }
        Py_XDECREF(pFunc);
        Py_DECREF(pModule);
    }
    else {
        PyErr_Print();
        fprintf(stderr, "Failed to load \"%s\"\n", argv[1]);
        return 1;
    }
    Py_Finalize();
    return 0;
}

这段代码从argv[1]中载入Python脚本,并且调用argv[2]中的函数,整数型的参数则是从argv数组后面得来的。如果编译和链接这个程序,执行如下脚本:

def multiply(a,b):
    print "Will compute",a,"times",b
    c=0
    for i in range(0,a)
        c=c+b
    return c

结果将是:

$ call multiply multiply 3 2
Will compute 3 times 2
Result of call: 6

虽然这个程序的代码挺多的,但是大部分其实都是做数据转换和错误报告。主要关于嵌入Python的开始于:

Py_Initialize();
pName=PyString_FromString(argv[1]);
/* Error checking of pName left out */
pModule=PyImport_Import(pName);

初始化解释器之后,使用 PyImport_Import() 导入模块。这个函数需要字符串作为参数,使用 PyString_FromString() 来构造:

pFunc=PyObject_GetAttrString(pModule,argv[2]);
/* pFunc is a new reference */
if (pFunc && PyCallable_Check(pFunc)) {
    ...
}
Py_XDECREF(pFunc);

载入了模块以后,就可以通过 PyObject_GetAttrString() 来获取对象。如果名字存在并且可以执行则可以安全的调用它。程序随后构造参数元组,然后执行调用:

pValue=PyObject_CallObject(pFunc,pArgs);

函数调用之后,pValue要么是NULL,要么是返回值的对象引用。注意在检查完返回值之后要释放引用。

4   扩展嵌入的Python

至今为止,嵌入的Python解释器还不能访问应用程序本身的功能。Python的API允许扩展嵌入的Python的解释器。所以,Python可以获得其所嵌入的程序的功能。这听起来挺麻烦的,其实并不是那样。只要简单的忘记是应用程序启动了Python解释器。

可以把程序看作一对功能的集合,可以写一些胶水代码来来让Python访问这些功能,有如你在写一个普通的Python扩展一样。例如:

static int numargs=0;

/* Return the number of arguments of the application command line */
static PyObject*
emb_numargs(PyObject *self, PyObject *args)
{
    if(!PyArg_ParseTuple(args, ":numargs"))
        return NULL;
    return Py_BuildValue("i", numargs);
}

static PyMethodDef EmbMethods[] = {
    {"numargs", emb_numargs, METH_VARARGS,
     "Return the number of arguments received by the process."},
    {NULL, NULL, 0, NULL}
};

添加上面的代码到 main() 函数。同样,插入如下两个语句到 Py_Initialize() 函数之后:

numargs=argc;
Py_InitModule("emb",EmbMethods);

这两行代码初始化numargs变量,并且使得 emb.numargs() 函数更加易于被Python嵌入的解释器所理解。通过这个扩展,Python脚本可以做如下事情:

import emb
print "Number of arguments",emb.numargs()

在实际的应用程序中,方法需要导出API以供Python使用。

5   在C++中嵌入Python

有时候需要将Python嵌入到C++程序中,而你必须有一些要注意的C++系统的细节,一般来说你要为这个程序写一个main()函数,然后使用C++编译器来编译和链接程序。而这里不需要因为使用C++而重新编译Python本身。

6   链接必备条件

configure 脚本执行时,可以正确的生成动态链接库使用的导出符号,而这些却不会自动被嵌入的静态链接的Python所继承,至少是在Unix。这是用于静态链接运行库(libpython.a)并且需要载入动态扩展(.so)的方式。

问题是一些入口点是使用Python运行时定义的而仅供扩展模块使用。如果嵌入应用不使用任何这些入口点,一些链接器不会包含这些实体到最终可执行文件的符号表。一些附加的选项可以用于告知连接器不要删除这些符号。

对于不同的平台,想要正确的检测该使用何种参数是非常困难的,但是幸运的是Python配置好了这些值。只要通过已经安装的Python解释器,启动交互解释器然后执行如下会话即可:

>>> import distutils.sysconfig
>>> distutils.sysconfig.get_config_var('LINKFORSHARED')
'-Xlinker -export-dynamic'

字符串的内容就是生成的选项。如果字符串为空,则不需要任何的附加选项。LINKFORSHARED的定义与Python顶层Makefile中的同名变量相同。

我看最近的利息变动

Tuesday, December 18th, 2007

我看最近的利息变动

国务院将在8月15日上调利息0.27%并将利息税从20%降低到5%,市场反应五花八门。各个国家对经济进行调控,比较常用的手段也就是存款利息的调整。当经济发展太慢时,适当的降低利息,促使市场上流通的货币(传说中的通货)提高,就会促进经济的发展。而在经济发展过热,泡沫严重时,可以提高利息,使得一部分流通货币进入银行,通货紧缩导致经济发展变慢。

其实对一个国家来说,经济发展并非越快越好,而是保持在一个健康的速度的附近比较合适。经济发展太慢,当然不利于整个国家的经济发展,但是在经济发展较慢的过程,确实一个经济结构调整的过程。可以使得产能过剩的一些行业进行内部的调整,通过经济杠杆使得优秀的企业得以留存。而经济发展太快时,诚然经济总量急剧扩大,但是同时大部分的商人要么目光短浅,要么出于赌博的心态,会有一些行业产能过剩,导致国家总体经济资源的浪费。这对国家经济的长远发展也是非常不利的。所以说,这次国家的利息调整还是很恰到好处的。

国家调高利息并降低利息税,实际上是从三个原因两个方向上降低流通货币量,来放低经济发展速度。对与储户方向,有两个原因,一是纯款利息的提高,二是利息税的降低,两个原因都会直接提高储户的总体利息收益。而对企业方向,则有一个原因,就是利息的提高其实是存款利息和贷款利息的同步提高。而贷款利息的提高将会使得一些企业放弃一些激进的扩张计划,进行内部调整。毕竟这样的贷款成本就会有所提高。

另外,参考本次利息上调的幅度来看,并非所有的存款品种都上调相同的幅度。活期存款只是在0.72%的基础上上调了0.09%,其他的零存整取定期存款则是上涨了0.27%。其他各项贷款的利率则提高了0.27%-0.28%,住房公积金贷款利率提高了0.09%。可见这次利息的调整还是有相当的针对性的,主要就是针对经济领域和资本市场,而对于个人住房贷款则要宽容的多。

可以见得,个人住房贷款的用户是个人,而国内的个人虽然对住房的需求量很大,但是由于个人住房公积金贷款的各项限制,却难以用于炒房。所以对个人住房公积金贷款的宽容并不会形成房价继续走高的理由。而面向经济领域和资本市场的贷款普遍提高了0.27%-0.28%则会比较有效的提高企业贷款成本。

从银行的角度讲,这次利率的调高也很有意思,单纯对比普通贷款和零存整取定期存款,银行的收入实际上是略有增加的,就在于部分贷款利率的提高略超过了定期存款利率。而对个人住房公积金贷款,则可以直接针对活期存款来计算。但是最终总的说来,其实银行的收入未必是增还是减,关键看一个数量。首先,存款利率的提高导致更多的资金进入银行,银行需要付出更多的存款利息,另一方面贷款会减少,贷款收入就会减少。所以银行的利息支出是存款利率*存款额,利息收入是贷款利率*贷款额。

好了,还是看看市场的反应吧。罗列几个腾讯新闻的标题。

“华生:加息措施对于经济的影响更大”,跟我刚才分析的差不多,主要还是因为太浅显。

“沈明高:属温和紧缩 年内还会加息一次”,我关注金融时间太短,至于是否温和还不知道,更不知道这个量的问题。

“曹凤岐:加息意料之中 非直接针对股市”,但是事实上股市的变动往往比市场剧烈,年初拿着养老金入市的人们这次也许可以考虑变现了。但是现在国内泡沫过多,确实应该调控一下了才是真的。

“尹中立:调控经济过热 对股市是大利空”,大利空是什么我不知道,但是我仔细看了这篇文档,尹中立确实是比较正直的,说话也客观些。股市和经济的疯狂会最终付出惨重的代价的。

“刘煜辉:加息不会对楼市股市有任何影响”,这个就基本上属于胡编乱造了。而事实上无论这个搞经济的所谓经济学家是混在市场上还是混在校园里,都无非是要创造一个贫富急剧分化的社会。他的这个言论也无非是为了托市,维持泡沫的扩大。包括张五常也说出过腐败比经济改革更有效率的话来。社会不是仅仅只有经济,如果只有经济、充满了资本阴谋,那国家也就没有未来了。强烈鄙视不负责任,反倒自称为经济学家的人。

“向威达:可能迎来股市“灿烂一笑””,整篇文章都没有写明白灿烂一笑是个什么东西,故弄玄虚。

“沈明高:有助提高股市理性气氛”,比较中庸的论点,无比的正确,但是其实跟没说差不多。

“贺强:市场已有与其 股市将低开高走”,没有看股市的习惯,不知是市场有预期,还是他自己有预期。如果股市不买国家经济调控的帐,只有两种可能,一种是所有股民都疯掉了(见80年代末的日本),二是背后黑手一直注入资金,两种都没什么好下场。

“易宪容:不加息房产泡沫必破”,是泡沫早晚都会破,只是有不同的方式,国家选择缓慢的方式让市场学会理智是个好事。只是被套住的人心理反应不会那么剧烈而已。

Intel vs AMD x264压缩速度对比

Tuesday, December 18th, 2007

Intel vs AMD x264压缩速度对比

AMD Athlon 64 3600+ Windows XP SP2 2core*1
21.71fps
AMD Opteron 2210 Centos 4.5 2Core*2 (1800GHz)
34.76fps
Intel Xeon E5310 1.60GHz Centos 4.5 4core*1
13.62fps

这就是我刚刚做的实测结果,对一个1.01GB的AVI文件进行x264压缩的速度对比。fps是每秒钟可以处理完成的视频帧数。说实在的,真的没想到Intel这么让我失望。尽管Intel的那颗4核至强并不高端,但是这种数倍的性能差距也不是随随便便就可以闹出来。谈谈详细吧。

有个网上下载的电影,用来做压缩测试(貌似网上下载的盗版电影都是用来给网友进行压缩测试的吧),顺便提下,我下载后24小时以内确实把这个片子删除了。片子的元信息如下:

VIDEO: [XVID] 720×480 24bpp 29.970 fps 2532.9 kbps (309.2kbyte/s)
AUDIO: 24000 Hz, 2 ch, s16le, 56.0 kbit/7.29% (ratio: 7000->96000)

压缩后的x264格式的原信息如下:

VIDEO: [H264] 720×480 24bpp 29.970 fps 971.2 kbps (118.6 kbyte/s)
AUDIO: 44100 Hz, 2 ch, s16le, 128.0 kbit/9.07% (ratio: 16000->176400)

主要压缩的目的就是使得文件变得更小,其中主要的一步是把码率从2532.9降低到1200,不过根据我双眼5.2的视力看来,压缩后没见画面质量的损失。这时的生成文件是535MB,后来因为对码率的概念不是很熟悉,就该为按照视频质量等级来压缩了。文件的尺寸倒是从1.01GB到了466MB。然后,就是开始了漫长的压缩,漫长到比看完原片时间还要长好久。压缩的这么慢主要也是因为x264格式的压缩率比较高,没办法。

谈谈我的3个实验环境:

第一个其实就是我自己用的台式机,用的Athlon 64 3600+ x2的CPU,1G的内存,还分给128MB给了显存。第二个是一台测试服务器,运行AMD的双路双核。第三个是Intel Xeon的单路4核。

实验环境使用的操作系统并不同,不过总体来说,至强服务器也是让我很失望的,居然慢到这个地步。同时也验证了在纯数学计算这个方面AMD的强大优势。

大体就这样吧,以后做视频转码肯定用AMD的服务器了。

使用C/C++扩展Python

Monday, December 10th, 2007

使用C/C++扩展Python

翻译: gashero

如果你会用C,实现Python嵌入模块很简单。利用扩展模块可做很多Python不方便做的事情,他们可以直接调用C库和系统调用。

为了支持扩展,Python API定义了一系列函数、宏和变量,提供了对Python运行时系统的访问支持。Python的C API由C源码组成,并包含 “Python.h” 头文件。

编写扩展模块与你的系统相关,下面会详解。

目录

1   一个简单的例子

下面的例子创建一个叫做 “spam” 的扩展模块,调用C库函数 system() 。这个函数输入一个NULL结尾的字符串并返回整数,可供Python调用方式如下:

>>> import spam
>>> status=spam.system("ls -l")

一个C扩展模块的文件名可以直接是 模块名.c 或者是 模块名module.c 。第一行应该导入头文件:

#include <Python.h>

这会导入Python API。

Warning

因为Python含有一些预处理定义,所以你必须在所有非标准头文件导入之前导入Python.h 。

Python.h中所有用户可见的符号都有 PyPY 的前缀,除非定义在标准头文件中。为了方便 “Python.h” 也包含了一些常用的标准头文件,包括<stdio.h>,<string.h>,<errno.h>,<stdlib.h>。如果你的系统没有后面的头文件,则会直接定义函数 malloc() 、 free() 和 realloc() 。

下面添加C代码到扩展模块,当调用 “spam.system(string)” 时会做出响应:

static PyObject*
spam_system(PyObject* self, PyObject* args) {
    const char* command;
    int sts;
    if (!PyArg_ParseTuple(args,"s",&command))
        return NULL;
    sts=system(command);
    return Py_BuildValue("i",sts);
}

调用方的Python只有一个命令参数字符串传递到C函数。C函数总是有两个参数,按照惯例分别叫做 self 和 args 。

self 参数仅用于用C实现内置方法而不是函数。本例中, self 总是为NULL,因为我们定义的是个函数,不是方法。这一切都是相同的,所以解释器也就不需要刻意区分两种不同的C函数。

args 参数是一个指向Python的tuple对象的指针,包含参数。每个tuple子项对应一个调用参数。这些参数也全都是Python对象,所以需要先转换成C值。函数 PyArg_ParseTuple() 检查参数类型并转换成C值。它使用模板字符串检测需要的参数类型。

PyArg_ParseTuple() 正常返回非零,并已经按照提供的地址存入了各个变量值。如果出错(零)则应该让函数返回NULL以通知解释器出错。

2   关于错误和异常

一个常见惯例是,函数发生错误时,应该设置一个异常环境并返回错误值(NULL)。异常存储在解释器静态全局变量中,如果为NULL,则没有发生异常。异常的第一个参数也需要保存在静态全局变量中,也就是raise的第二个参数。第三个变量包含栈回溯信息。这三个变量等同于Python变量 sys.exc_type 、 sys.exc_value 、 sys.exc_traceback 。这对找到错误是很必要的。

Python API中定义了一些函数来设置这些变量。

最常用的就是 PyErr_SetString() 。参数是异常对象和C字符串。异常对象一般由像 PyExc_ZeroDivisionError 这样的对象来预定义。C字符串指明异常原因,并最终存储在异常的第一个参数里面。

另一个有用的函数是 PyErr_SetFromErrno() ,仅接受一个异常对象,异常描述包含在全局变量 errno 中。最通用的函数还是 PyErr_SetObject() ,包含两个参数,分别为异常对象和异常描述。你不需要使用 Py_INCREF() 来增加传递到其他函数的参数对象的引用计数。

你可以通过 PyErr_Occurred() 获知当前异常,返回当前异常对象,如果确实没有则为NULL。一般来说,你在调用函数时不需要调用 PyErr_Occurred() 检查是否发生了异常,你可以直接检查返回值。

如果调用更下层函数时出错了,那么本函数返回NULL表示错误,并且整个调用栈中只要有一处调用 PyErr_*() 函数设置异常就可以。一般来说,首先发现错误的函数应该设置异常。一旦这个错误到达了Python解释器的主循环,则会中断当前执行代码并追究异常。

有一种情况下,模块可能依靠其他 PyErr_*() 函数给出更加详细的错误信息,并且是正确的。但是按照一般规则,这并不重要,很多操作都会因为种种原因而挂掉。

想要忽略这些函数设置的异常,异常情况必须明确的使用 PyErr_Clear() 来清除。只有在C代码想要自己处理异常而不是传给解释器时才这么做。

每次失败的 malloc() 调用必须抛出一个异常,直接调用 malloc() 或 realloc() 的地方要调用 PyErr_NoMemory() 并返回错误。所有创建对象的函数都已经实现了这个异常的抛出,所以这是每个分配内存都要做的。

还要注意的是 PyArg_ParseTuple() 系列函数的异常,返回一个整数状态码是有效的,0是成功,-1是失败,有如Unix系统调用。

最后,小心垃圾情理,也就是 Py_XDECREF() 和 Py_DECREF() 的调用,会返回的异常。

选择抛出哪个异常完全是你的个人爱好了。有一系列的C对象代表了内置Python异常,例如 PyExc_ZeroDivisionError ,你可以直接使用。当然,你可能选择更合适的异常,不过别使用 PyExc_TypeError 告知文件打开失败(有个更合适的 PyExc_IOError )。如果参数列表有误, PyArg_ParseTuple() 通常会抛出 PyExc_TypeError 。如果参数值域有误, PyExc_ValueError 更合适一些。

你也可以为你的模块定义一个唯一的新异常。需要在文件前部声明一个静态对象变量,如:

static PyObject* SpamError;

然后在模块初始化函数(initspam())里面初始化它,并省却了处理:

PyMODINIT_FUNC
initspam(void) {
    PyObject* m;
    m=Py_InitModule("spam",SpamMethods);
    if (m==NULL)
        return NULL;
    SpamError=PyErr_NewException("spam.error",NULL,NULL);
    Py_INCREF(SpamError);
    PyModule_AddObject(m,"error",SpamError);
}

注意实际的Python异常名字是 spam.error 。 PyErr_NewException() 函数使用Exception为基类创建一个类(除非是使用另外一个类替代NULL)。

同样注意的是创建类保存了SpamError的一个引用,这是有意的。为了防止被垃圾回收掉,否则SpamError随时会成为野指针。

一会讨论 PyMODINIT_FUNC 作为函数返回类型的用法。

3   回到例子

回到前面的例子,你应该明白下面的代码:

if (!PyArg_ParseTuple(args,"s",&command))
    return NULL;

就是为了报告解释器一个异常。如果执行正常则变量会拷贝到本地,后面的变量都应该以指针的方式提供,以方便设置变量。本例中的command会被声明为 “const char* command” 。

下一个语句使用UNIX系统函数system(),传递给他的参数是刚才从 PyArg_ParseTuple() 取出的:

sts=system(command);

我们的 spam.system() 函数必须返回一个PY对象,这可以通过 Py_BuildValue() 来完成,其形式与 PyArg_ParseTuple() 很像,获取格式字符串和C值,并返回新的Python对象:

return Py_BuildValue("i",sts);

在这种情况下,会返回一个整数对象,这个对象会在Python堆里面管理。

如果你的C函数没有有用的返回值,则必须返回None。你可以用 Py_RETUN_NONE 宏来完成:

Py_INCREF(Py_None);
return Py_None;

Py_None 是一个C名字指定Python对象None。这是一个真正的PY对象,而不是NULL指针。

4   模块方法表和初始化函数

把函数声明为可以被Python调用,需要先定义一个方法表:

static PyMethodDef SpamMethods[]= {
    ...
    {"system",spam_system,METH_VARARGS,
    "Execute a shell command."},
    ...
    {NULL,NULL,0,NULL}    /*必须的结束符*/
};

注意第三个参数 METH_VARARGS ,这个标志指定会使用C的调用惯例。可选值有 METH_VARARGS 、 METH_VARARGS | METH_KEYWORDS 。值0代表使用 PyArg_ParseTuple() 的陈旧变量。

如果单独使用 METH_VARARGS ,函数会等待Python传来tuple格式的参数,并最终使用 PyArg_ParseTuple() 进行解析。

METH_KEYWORDS 值表示接受关键字参数。这种情况下C函数需要接受第三个 PyObject* 对象,表示字典参数,使用 PyArg_ParseTupleAndKeywords() 来解析出参数。

方法表必须传递给模块初始化函数。初始化函数函数名规则为 initname() ,其中 name 为模块名。并且不能定义为文件中的static函数:

PyMODINIT_FUNC
initspam(void) {
    (void) Py_InitModule("spam",SpamMethods);
}

注意 PyMODINIT_FUNC 声明了void为返回类型,还有就是平台相关的一些定义,如C++的就要定义成 extern “C” 。

Python程序首次导入这个模块时就会调用initspam()函数。他调用 Py_InitModule() 来创建一个模块对象,同时这个模块对象会插入到 sys.modules 字典中的 “spam” 键下面。然后是插入方法表中的内置函数到 “spam” 键下面。 Py_InitModule() 返回一个指针指向刚创建的模块对象。他是有可能发生严重错误的,也有可能在无法正确初始化时返回NULL。

当嵌入Python时, initspam() 函数不会自动被调用,除非在入口处的 _PyImport_Inittab 表。最简单的初始化方法是在 Py_Initialize() 之后静态调用 initspam() 函数:

int
main(int argc, char* argv[]) {
    Py_SetProgramName(argv[0]);
    Py_Initialize();
    initspam();
    //...
}

在Python发行版的 Demo/embed/demo.c 中有可以参考的源码。

Note

从 sys.modules 中移除模块入口,或者在多解释器环境中导入编译模块,会导致一些扩展模块出错。扩展模块作者应该特别注意初始化内部数据结构。同时要注意 reload() 函数可能会被用在扩展模块身上,并调用模块初始化函数,但是对动态状如对象(动态链接库),却不会重新载入。

更多关于模块的现实的例子包含在Python源码包的Modules/xxmodule.c中。这些文件可以用作你的代码模板,或者学习。脚本 modulator.py 包含在源码发行版或Windows安装中,提供了一个简单的GUI,用来声明需要实现的函数和对象,并且可以生成供填入的模板。脚本在 Tools/modulator/ 目录。查看README以了解用法。

5   编译和连接

如果使用动态载入,细节依赖于系统,查看关于构建扩展模块部分,和关于在Windows下构建扩展的细节。

如果你无法使用动态载入,或者希望模块成为Python的永久组成部分,就必须改变配置并重新构建解释器。幸运的是,这对UNIX来说很简单,只要把你的代码(例如spammodule.c)放在 Modules/ Python源码目录下,然后增加一行到文件 Modules/Setup.local 来描述你的文件即可:

spam spammodule.o

然后重新构建解释器,使用make。你也可以在 Modules/ 子目录使用make,但是你接下来首先要重建Makefile文件,使用 make Makefile 命令。这对你改变 Setup 文件来说很重要。

如果你的模块需要其他扩展模块连接,则需要在配置文件后面加入,如:

spam spammodule.o -lX11

6   在C中调用Python函数

迄今为止,我们一直把注意力集中于让Python调用C函数,其实反过来也很有用,就是用C调用Python函数。这在回调函数中尤其有用。如果一个C接口使用回调,那么就要实现这个回调机制。

幸运的是,Python解释器是比较方便回调的,并给标准Python函数提供了标准接口。这里就不再详述解析Python代码作为输入的方式,如果有兴趣可以参考 Python/pythonmain.c 中的 -c 命令代码。

调用Python函数,首先Python程序要传递Python函数对象。当调用这个函数时,用全局变量保存Python函数对象的指针,还要调用 Py_INCREF() 来增加引用计数,当然不用全局变量也没什么关系。例如如下:

static PyObject* my_callback=NULL;
static PyObject*
my_set_callback(PyObject* dummy, PyObject* args) {
    PyObject* result=NULL;
    PyObject* temp;
    if (PyArg_ParseTuple(args,"O:set_callback",&temp)) {
        if (!PyCallable_Check(temp)) {
            PyErr_SetString(PyExc_TypeError,"parameter must be callable");
            return NULL;
        }
        Py_XINCREF(temp);
        Py_XINCREF(my_callback);
        my_callback=temp;
        Py_INCREF(Py_None);
        result=Py_None;
    }
    return result;
}

这个函数必须使用 METH_VARARGS 标志注册到解释器。宏 Py_XINCREF() 和 Py_XDECREF() 增加和减少对象的引用计数。

然后,就要调用函数了,使用 PyEval_CallObject() 。这个函数有两个参数,都是指向Python对象:Python函数和参数列表。参数列表必须总是tuple对象,如果没有参数则要传递空的tuple。使用 Py_BuildValue() 时,在圆括号中的参数会构造成tuple,无论有没有参数,如:

int arg;
PyObject* arglist;
PyObject* result;
//...
arg=123;
//...
arglist=Py_BuildValue("(i)",arg);
result=PyEval_CallObject(my_callback,arglist);
Py_DECREF(arglist);

PyEval_CallObject() 返回一个Python对象指针表示返回值。 PyEval_CallObject() 是 引用计数无关 的,有如例子中,参数列表对象使用完成后就立即减少引用计数了。`PyEval_CallObject()` 返回一个Python对象指针表示返回值。 PyEval_CallObject() 是 引用计数无关 的,有如例子中,参数列表对象使用完成后就立即减少引用计数了。

PyEval_CallObject() 的返回值总是新的,新建对象或者是对已有对象增加引用计数。所以你必须获取这个对象指针,在使用后减少其引用计数,即便是对返回值没有兴趣也要这么做。但是在减少这个引用计数之前,你必须先检查返回的指针是否为NULL。如果是NULL,则表示出现了异常并中止了。如果没有处理则会向上传递并最终显示调用栈,当然,你最好还是处理好异常。如果你对异常没有兴趣,可以用 PyErr_Clear() 清除异常,例如:

if (result==NULL)
    return NULL;  /*向上传递异常*/
//使用result
Py_DECREF(result);

依赖于具体的回调函数,你还要提供一个参数列表到 PyEval_CallObject() 。在某些情况下参数列表是由Python程序提供的,通过接口再传到回调函数。这样就可以不改变形式直接传递。另外一些时候你要构造一个新的tuple来传递参数。最简单的方法就是 Py_BuildValue() 函数构造tuple。例如,你要传递一个事件对象时可以用:

PyObject* arglist;
//...
arglist=Py_BuildValue("(l)",eventcode);
result=PyEval_CallObject(my_callback,arglist);
Py_DECREF(arglist);
if (result==NULL)
    return NULL;  /*一个错误*/
/*使用返回值*/
Py_DECREF(result);

注意 Py_DECREF(arglist) 所在处会立即调用,在错误检查之前。当然还要注意一些常规的错误,比如 Py_BuildValue() 可能会遭遇内存不足等等。

7   解析传给扩展模块函数的参数

函数 PyArg_ParseTuple() 声明如下:

int PyArg_ParseTuple(PyObject* arg, char* format, ...);

参数 arg 必须是一个tuple对象,包含传递过来的参数, format 参数必须是格式化字符串,语法解释见 “Python C/API” 的5.5节。剩余参数是各个变量的地址,类型要与格式化字符串对应。

注意 PyArg_ParseTuple() 会检测他需要的Python参数类型,却无法检测传递给他的C变量地址,如果这里出错了,可能会在内存中随机写入东西,小心。

任何Python对象的引用,在调用者这里都是 借用的引用 ,而不增加引用计数。

一些例子:

int ok;
int i,j;
long k,l;
const char* s;
int size;
ok=PyArg_ParseTuple(args,"");
/* python call: f() */

ok=PyArg_ParseTuple(args,"s",&s);
/* python call: f('whoops!') */

ok=PyArg_ParseTuple(args,"lls",&k,&l,&s);
/* python call: f(1,2,'three') */

ok=PyArg_ParseTuple(args,"(ii)s#",&i,&j,&s,&size);
/* python call: f((1,2),'three') */

{
    const char* file;
    const char* mode="r";
    int bufsize=0;
    ok=PyArg_ParseTuple(args,"s|si",&file,&mode,&bufsize);
    /* python call:
        f('spam')
        f('spam','w')
        f('spam','wb',100000)
    */
}

{
    int left,top,right,bottom,h,v;
    ok=PyArg_ParseTuple(args,"((ii)(ii))(ii)",
        &left,&top,&right,&bottom,&h,&v);
    /* python call: f(((0,0),(400,300)),(10,10)) */
}

{
    Py_complex c;
    ok=PyArg_ParseTuple(args,"D:myfunction",&c);
    /* python call: myfunction(1+2j) */
}

8   解析传给扩展模块函数的关键字参数

函数 PyArg_ParseTupleAndKeywords() 声明如下:

int PyArg_ParseTupleAndKeywords(PyObject* arg, PyObject* kwdict, char* format, char* kwlist[],...);

参数arg和format定义同 PyArg_ParseTuple() 。参数 kwdict 是关键字字典,用于接受运行时传来的关键字参数。参数 kwlist 是一个NULL结尾的字符串,定义了可以接受的参数名,并从左到右与format中各个变量对应。如果执行成功 PyArg_ParseTupleAndKeywords() 会返回true,否则返回false并抛出异常。

Note

嵌套的tuple在使用关键字参数时无法生效,不在kwlist中的关键字参数会导致 TypeError 异常。

如下是使用关键字参数的例子模块,作者是 Geoff Philbrick (phibrick@hks.com):

#include "Python.h"

static PyObject*
keywdarg_parrot(PyObject* self, PyObject* args, PyObject* keywds) {
    int voltage;
    char* state="a stiff";
    char* action="voom";
    char* type="Norwegian Blue";
    static char* kwlist[]={"voltage","state","action","type",NULL};
    if (!PyArg_ParseTupleAndKeywords(args,keywds,"i|sss",kwlist,
            &voltage,&state,&action,&type))
        return NULL;
    printf("-- This parrot wouldn't %s if you put %i Volts through it.\n",action,voltage);
    printf("-- Lovely plumage, the %s -- It's %s!\n",type,state);
    Py_INCREF(Py_None);
    return Py_None;
}

static PyMethodDef keywdary_methods[]= {
    /*注意PyCFunction,这对需要关键字参数的函数很必要*/
    {"parrot",(PyCFunction)keywdarg_parrot, METH_VARARGS | METH_KEYWORDS,"Print a lovely skit to standard output."},
    {NULL,NULL,0,NULL}
};

void
initkeywdarg(void) {
    Py_InitModule("keywdarg",keywdarg_methods);
}

9   构造任意值

这个函数声明与 PyArg_ParseTuple() 很相似,如下:

PyObject* Py_BuildValue(char* format, ...);

接受一个格式字符串,与 PyArg_ParseTuple() 相同,但是参数必须是原变量的地址指针。最终返回一个Python对象适合于返回给Python代码。

一个与 PyArg_ParseTuple() 的不同是,后面可能需要的要求返回一个tuple,比如用于传递给其他Python函数以参数。 Py_BuildValue() 并不总是生成tuple,在多于1个参数时会生成tuple,而如果没有参数则返回None,一个参数则直接返回该参数的对象。如果要求强制生成一个长度为空的tuple,或包含一个元素的tuple,需要在格式字符串中加上括号。

例如:

代码 返回值
Py_BuildValue("") None
Py_BuildValue("i",123) 123
Py_BuildValue("iii",123,456,789) (123,456,789)
Py_BuildValue("s","hello") ‘hello’
Py_BuildValue("ss","hello","world") (‘hello’, ‘world’)
Py_BuildValue("s#","hello",4) ‘hell’
Py_BuildValue("()") ()
Py_BuildValue("(i)",123) (123,)
Py_BuildValue("(ii)",123,456) (123,456)
Py_BuildValue("(i,i)",123,456) (123,456)
Py_BuildValue("[i,i]",123,456) [123,456]
Py_BuildValue("{s:i,s:i}",'a',1,'b',2) {‘a':1,’b':2}
Py_BuildValue("((ii)(ii))(ii)",1,2,3,4,5,6) (((1,2),(3,4)),(5,6))

10   引用计数

在C/C++语言中,程序员负责动态分配和回收堆(heap)当中的内存。这意味着,我们在C中编程时必须面对这个问题。

每个由 malloc() 分配的内存块,最终都要由 free() 扔到可用内存池里面去。而调用 free() 的时机非常重要,如果一个内存块忘了 free() 则是内存泄漏,程序结束前将无法重新使用。而如果对同一内存块 free() 了以后,另外一个指针再次访问,则叫做野指针。这同样会导致严重的问题。

内存泄露往往发生在一些并不常见的程序流程上面,比如一个函数申请了资源以后,却提前返回了,返回之前没有做清理工作。人们经常忘记释放资源,尤其对于后加新加的代码,而且会长时间都无法发现。这些函数往往并不经常调用,而且现在大多数机器都有庞大的虚拟内存,所以内存泄漏往往在长时间运行的进程,或经常被调用的函数中才容易发现。所以最好有个好习惯加上代码约定来尽量避免内存泄露。

Python往往包含大量的内存分配和释放,同样需要避免内存泄漏和野指针。他选择的方法就是 引用计数 。其原理比较简单:每个对象都包含一个计数器,计数器的增减与引用的增减直接相关,当引用计数为0时,表示对象已经没有存在的意义了,就可以删除了。

一个叫法是 自动垃圾回收 ,引用计数是一种垃圾回收方法,用户必须要手动调用 free() 函数。优点是可以提高内存使用率,缺点是C语言至今也没有一个可移植的自动垃圾回收器。引用计数却可以很好的移植,有如C当中的 malloc() 和 free() 一样。也许某一天会出现C语言饿自动垃圾回收器,不过在此之前我们还得用引用计数。

Python使用传统的引用计数实现,不过他包含一个循环引用探测器。这允许应用不需要担心的直接或间接的创建循环引用,而这实际上是引用计数实现的自动垃圾回收的致命缺点。循环引用指对象经过几层引用后回到自己,导致了其引用计数总是不为0。传统的引用计数实现无法解决循环引用的问题,尽管已经没有其他外部引用了。

循环引用探测器可以检测出垃圾回收中的循环并释放其中的对象。只要Python对象有 __del__() 方法,Python就可以通过 gc module 模块来自动暴露出循环引用。gc模块还提供 collect() 函数来运行循环引用探测器,可以在配置文件或运行时禁用循环应用探测器。

循环引用探测器作为一个备选选项,默认是打开的,可以在构建时使用 –without-cycle-gc 选项加到 configure 上来配置,或者移除 pyconfig.h 文件中的 WITH_CYCLE_GC 宏定义。在循环引用探测器禁用后,gc模块将不可用。

10.1   Python中的引用计数

有两个宏 Py_INCREF(x) 和 Py_DECREF(x) 用于增减引用计数。 Py_DECREF() 同时会在引用计数为0时释放对象资源。为了灵活性,他并不是直接调用 free() 而是调用对象所在类型的析构函数。

一个大问题是何时调用 Py_INCREF(x) 和 Py_DECREF(x) 。首先介绍一些术语。没有任何人都不会 拥有 一个对象,只能拥有其引用。对一个对象的引用计数定义了引用数量。拥有的引用,在不再需要时负责调用 Py_DECREF() 来减少引用计数。传递引用计数有三种方式:传递、存储和调用 Py_DECREF() 。忘记减少拥有的引用计数会导致内存泄漏。

同样重要的一个概念是 借用 一个对象,借用的对象不能调用 Py_DECREF() 来减少引用计数。借用者在不需要借用时,不保留其引用就可以了。应该避免拥有者释放对象之后仍然访问对象,也就是野指针。

借用的优点是你无需管理引用计数,缺点是可能被野指针搞的头晕。借用导致的野指针问题常发生在看起来无比正确,但是事实上已经被释放的对象。

借用的引用也可以用 Py_INCREF() 来改造成拥有的引用。这对引用的对象本身没什么影响,但是拥有引用的程序有责任在适当的时候释放这个拥有。

10.2   拥有规则

一个对象的引用进出一个函数时,其引用计数也应该同时改变。

大多数函数会返回一个对对象拥有的引用。而且几乎所有的函数其实都会创建一个对象,例如 PyInt_FromLong() 和 Py_BuildValue() ,传递一个拥有的引用给接受者。即便不是刚创建的,你也需要接受一个新的拥有引用。一般来说, PyInt_FromLong() 会维护一个常用值缓存,并且返回缓存项的引用。

很多函数提取一些对象的子对象并传递拥有引用,例如 PyObject_GetAttrString() 。另外,小心一些函数,包括: PyTuple_GetItem() 、 PyList_GetItem() 、 PyDict_GetItem() 和 PyDict_GetItemString() ,他们返回的都是借用的引用。

函数 PyImport_AddModule() 也是返回借用的引用,尽管他实际上创建了对象,只不过其拥有的引用实际存储在了 sys.modules 中。

当你传递一个对象的引用到另外一个函数时,一般来说,函数是借用你的引用,如果他确实需要存储,则会使用 Py_INCREF() 来变为拥有引用。这个规则有两种可能的异常: PyTuple_SetItem() 和 PyList_SetItem() ,这两个函数获取传递给他的拥有引用,即便是他们执行出错了。不过 PyDict_SetItem() 却不是接收拥有的引用。

当一个C函数被py调用时,使用对参数的借用。调用者拥有参数对象的拥有引用。所以,借用的引用的寿命是函数返回。只有当这类参数必须存储时,才会使用 Py_INCREF() 变为拥有的引用。

从C函数返回的对象引用必须是拥有的引用,这时的拥有者是调用者。

10.3   危险的薄冰

有些使用借用的情况会出现问题。这是对解释器的盲目理解所导致的,因为拥有者往往提前释放了引用。

首先而最重要的情况是使用 Py_DECREF() 来释放一个本来是借用的对象,比如列表中的元素:

void
bug(PyObject* list) {
    PyObject* item=PyList_GetItem(list,0);
    PyList_SetItem(list,1,PyInt_FromLong(0L));
    PyObject_Print(item,stdout,0); /* BUG! */
}

这个函数首先借用了 list[0] ,然后把 list[1] 替换为值0,最后打印借用的引用。看起来正确么,不是!

我们来跟踪一下 PyList_SetItem() 的控制流,列表拥有所有元素的引用,所以当项目1被替换时,他就释放了原始项目1。而原始项目1是一个用户定义类的实例,假设这个类定义包含 __del__() 方法。如果这个类的实例引用计数为1,处理过程会调用 __del__() 方法。

因为使用python编写,所以 __del__() 中可以用任何python代码来完成释放工作。替换元素的过程会执行 del list[0] ,即减掉了对象的最后一个引用,然后就可以释放内存了。

知道问题后,解决方案就出来了:临时增加引用计数。正确的版本如下:

void
no_bug(PyObject* list) {
    PyObject* item=PyList_GetItem(list,0);
    Py_INCREF(item);
    PyList_SetItem(list,1,PyInt_FromLong(0L));
    PyObject_Print(item,stdout,0);
    Py_DECREF(item);
}

这是一个真实的故事,旧版本的Python中多处包含这个问题,让guido花费大量时间研究 __del__() 为什么失败了。

第二种情况的问题出现在多线程中的借用引用。一般来说,python中的多线程之间并不能互相影响对方,因为存在一个GIL。不过,这可能使用宏 Py_BEGIN_ALLOW_THREADS 来临时释放锁,最后通过宏 Py_END_ALLOW_THREADS 来再申请锁,这在IO调用时很常见,允许其他线程使用处理器而不是等待IO结束。很明显,下面的代码与前面的问题相同:

void
bug(PyObject* list) {
    PyObject* item=PyList_GetItem(list,0);
    Py_BEGIN_ALLOW_THREADS
    //一些IO阻塞调用
    Py_END_ALLOW_THREADS
    PyObject_Print(item,stdout,0); /*BUG*/
}

10.4   NULL指针

一般来说,函数接受的参数并不希望你传递一个NULL指针进来,这会出错的。函数的返回对象引用返回NULL则代表发生了异常。这是Python的机制,毕竟,一个函数如果执行出错了,那么也没有必要多解释了,浪费时间。(注:彪悍的异常也不需要解释)

最好的测试NULL的方法就是在代码里面,一个指针如果收到了NULL,例如 malloc() 或其他函数,则表示发生了异常。

宏 Py_INCREF() 和 Py_DECREF() 并不检查NULL指针,不过还好, Py_XINCREF() 和 Py_XDECREF() 会检查。

检查特定类型的宏,形如 Pytype_Check() 也不检查NULL指针,因为这个检查是多余的。

C函数的调用机制确保传递的参数列表(也就是args参数)用不为NULL,事实上,它总是一个tuple。

而把NULL扔到Python用户那里可就是一个非常严重的错误了。

11   使用C++编写扩展

有时候需要用C++编写Python扩展模块。不过有一些严格的限制。如果Python解释器的主函数是使用C编译器编译和连接的,那么全局和静态对象的构造函数将无法使用。而主函数使用C++编译器时则不会有这个问题。被Python调用的函数,特别是模块初始化函数,必须声明为 extern "C" 。没有必要在Python头文件中使用 extern "C" 因为在使用C++编译器时会自动加上 __cplusplus 这个定义,而一般的C++编译器一般都会设置这个符号。

12   提供给其他模块以C API

很多模块只是提供给Python使用的函数和新类型,但是偶尔也有可能被其他扩展模块所调用。例如一个模块实现了 “collection” 类型,可以像list一样工作而没有顺序。有如标准Python中的list类型一样,提供的C接口可以让扩展模块创建和管理list,这个新的类型也需要有C函数以供其他扩展模块直接管理。

初看这个功能可能以为很简单:只要写这些函数就行了(不需要声明为静态),提供适当的头文件,并注释C的API。当然,如果所有的扩展模块都是静态链接到Python解释器的话,这当然可以正常工作。但是当其他扩展模块是动态链接库时,定义在一个模块中的符号,可能对另外一个模块来说并不是可见的。而这个可见性又是依赖操作系统实现的,一些操作系统对Python解释器使用全局命名空间和所有的扩展模块(例如Windows),也有些系统则需要明确的声明模块的导出符号表(AIX就是个例子),或者提供一个不同策略的选择(大多数的Unices)。即便这些符号是全局可见的,拥有函数的模块,也可能尚未载入。

为了可移植性,不要奢望任何符号会对外可见。这意味着模块中所有的符号都声明为 static ,除了模块的初始化函数以外,这也是为了避免各个扩展模块之间的符号名称冲突。这也意味着必须以其他方式导出扩展模块的符号。

Python提供了一种特殊的机制,以便在扩展模块间传递C级别的信息(指针): CObject 。一个CObject是一个Python的数据类型,存储了任意类型指针(void*)。CObject可以只通过C API来创建和存取,但是却可以像其他Python对象那样来传递。在特别的情况下,他们可以被赋予一个扩展模块命名空间内的名字。其他扩展模块随后可以导入这个模块,获取这个名字的值,然后得到CObject中保存的指针。

通过CObject有很多种方式导出扩展模块的C API。每个名字都可以得到他自己的CObject,或者可以把所有的导出C API放在一个CObject指定的数组中来发布。所以可以有很多种方法导出C API。

如下的示例代码展示了把大部分的重负载任务交给扩展模块,作为一个很普通的扩展模块的例子。他保存了所有的C API的指针到一个数组中,而这个数组的指针存储在CObject中。对应的头文件提供了一个宏以管理导入模块和获取C API的指针,客户端模块只需要在存取C API之前执行这个宏就可以了。

这个导出模块是修改自1.1节的spam模块。函数 spam.system() 并不是直接调用C库的函数 system() ,而是调用 PySpam_System() ,提供了更加复杂的功能。这个函数 PySpam_System() 同样导出供其他扩展模块使用。

函数 PySpam_System() 是一个纯C函数,声明为static如下:

static int
PySpam_System(const char* command) {
    return system(command);
}

函数 spam_system() 做了细小的修改:

static PyObject*
spam_system(PyObject* self, PyObject* args) {
    const char* command;
    int sts;
    if (!PyArg_ParseTuple(args,"s",&command))
        return NULL;
    sts=PySpam_System(command);
    return Py_BuildValue("i",sts);
}

在模块的头部加上如下行:

#include "Python.h"

另外两行需要添加的是:

#define SPAM_MODULE
#include "spammodule.h"

这个宏定义是告诉头文件需要作为导出模块,而不是客户端模块。最终模块的初始化函数必须管理初始化C API指针数组的初始化:

PyMODINIT_FUNC
initspam(void)
{
    PyObject *m;
    static void *PySpam_API[PySpam_API_pointers];
    PyObject *c_api_object;

    m = Py_InitModule("spam", SpamMethods);
    if (m == NULL)
        return;

    /* Initialize the C API pointer array */
    PySpam_API[PySpam_System_NUM] = (void *)PySpam_System;

    /* Create a CObject containing the API pointer array's address */
    c_api_object = PyCObject_FromVoidPtr((void *)PySpam_API, NULL);

    if (c_api_object != NULL)
        PyModule_AddObject(m, "_C_API", c_api_object);
}

注意 PySpam_API 声明为static,否则 initspam() 函数执行之后,指针数组就消失了。

大部分的工作还是在头文件 spammodule.h 中,如下:

#ifndef Py_SPAMMODULE_H
#define Py_SPAMMODULE_H
#ifdef __cplusplus
extern "C" {
#endif

/* Header file for spammodule */

/* C API functions */
#define PySpam_System_NUM 0
#define PySpam_System_RETURN int
#define PySpam_System_PROTO (const char *command)

/* Total number of C API pointers */
#define PySpam_API_pointers 1

#ifdef SPAM_MODULE
/* This section is used when compiling spammodule.c */

static PySpam_System_RETURN PySpam_System PySpam_System_PROTO;

#else
/* This section is used in modules that use spammodule's API */

static void **PySpam_API;

#define PySpam_System \
 (*(PySpam_System_RETURN (*)PySpam_System_PROTO) PySpam_API[PySpam_System_NUM])

/* Return -1 and set exception on error, 0 on success. */
static int
import_spam(void)
{
    PyObject *module = PyImport_ImportModule("spam");

    if (module != NULL) {
        PyObject *c_api_object = PyObject_GetAttrString(module, "_C_API");
        if (c_api_object == NULL)
            return -1;
        if (PyCObject_Check(c_api_object))
            PySpam_API = (void **)PyCObject_AsVoidPtr(c_api_object);
        Py_DECREF(c_api_object);
    }
    return 0;
}

#endif

#ifdef __cplusplus
}
#endif

#endif /* !defined(Py_SPAMMODULE_H) */

想要调用 PySpam_System() 的客户端模块必须在初始化函数中调用 import_spam() 以初始化导出扩展模块:

PyMODINIT_FUNC
initclient(void) {
    PyObject* m;
    m=Py_InitModule("client",ClientMethods);
    if (m==NULL)
        return;
    if (import_spam()<0)
        return;
    /*其他初始化语句*/
}

这样做的缺点是 spammodule.h 有点复杂。不过这种结构却可以方便的用于其他导出函数,所以学着用一次也就好了。

最后需要提及的是CObject提供的一些附加函数,用于CObject指定的内存块的分配和释放。详细信息可以参考Python的C API参考手册的CObject一节,和CObject的实现,参考文件 Include/cobject.hObjects/cobject.c

风险投资入门

Saturday, December 1st, 2007

风险投资入门

风险投资呢,就是传说中的“疯投”,以高风险的投资换取高利润。

风险投资是由专门的风险投资基金负责管理的,而这个管理的核心就是一大堆的投资顾问。一般来说,这些人都是经验非常丰富的,在提高利润,降低风险方面手段奇特。不过说实在的,他们其实都挺俗的,不要看作神仙就是了。

风险投资比较感兴趣,并且希望投资的项目都是需要高增长的。一般来说多见于高科技行业,比如最近10年内的互联网。对于传统行业,疯投则比较关注连锁的概念,举个例子国美,或者是麦当劳。高科技行业纵然是依靠几个人的天才,几十个人的补充,就创造出巨额利润的好途径。而连锁概念,则是做好一家店之后快速的复制并成倍的扩大收入。至于传统行业的连锁,我知道的不多,就先谈谈高科技行业里面的互联网的疯投流程。

一般来说呢,一个项目的最开始就是一个人头脑中的想法,想法想要变成行动就成了整个流程最为困难的一步。有的人,安于现状,于是放弃了;有的人,不想离开公司、学校,所以也放弃了。所以最后能真正动手的已经是少数了,有投入必有风险,这个就看每个人对风险的承受能力了。于是,想法要变成行动了。

大多数创业都是以团队的形式,其实所谓团队也就是两三个人为多,大家一起凑点钱,辞了工作,租个小房子,于是开始动手做。这个时间是整个团队最黑暗的时候,不过也是最单纯的时候。大家不分日夜的拼搏,做出理想中的东西。于是在一段时间之后,产品出来了,对于互联网,多半就是一个网站的上线。

这里谈谈创业团队。创业本身是高风险的事情,所以往往是最有毅力,并且聪明的一塌糊涂的人才会愿意参与。当然,创业初期,资金紧缺,大家基本上只是确保生存而已;更不能去招聘其他详细工作的工程师。所以,这个时候,创业团队的成员就需要身兼数职,非超人不能已矣。所以呢,选团队成员时还是谨慎些,仅仅有热情是不够的。这个阶段也不能带希望依靠团队生活的人进来。

在产品面世后,其实都很多团队来说,资金已经开始见底了。这时就需要风险投资了,团队选出一个leader去找风险投资商谈判,出让一部分股份,以换取发展所必需的资金。为了降低风险,很多风险投资项目都不是一家疯投全额投资的,而是三五家疯投共同投资,这个时候就需要挨家去谈。用自己的发展计划和方案来感动投资顾问的口水。

第一轮疯投拿到了之后,团队就可以考虑去注册个公司了,当然这个事情早点做貌似也没什么。公司也会在疯投的沐浴下快速成长。不过,风投基金并不会放心的把钱交给你就算了,而是一般会再派驻一个财务相关的职位给你,甚至是派一个CEO给你。这样的人一般就叫做投资监管,也许会顺便给企业的发展做点贡献。

再之后呢,就是不停的发展,第二轮,第三轮疯投等等。不过注意的几点是,很少有公司去申请第四轮风险投资。再有,并不是每个公司都可以安然的申请到下一轮的疯投,如果公司发展并不顺利,那么疯投可能会对公司的发展失去信心,于是便任由公司关门或收购。对现代企业来说,资金链的重要性怎么形容都不过分。创业企业也有至少99%是死于资金链的断裂。所以,像百度那样,申请到疯投以后也要做好未来可能资金链断裂的准备,尽可能的用这些疯投把公司维持的久一些,是很重要的。

最后呢,当然是IPO了,意思是“首次公开募股”,国内一般叫“上市”。其实也就是到某个证券交易所注册一下,把自己公司的股票再卖出去一部分,以换取下一步发展的资金。而证券交易所也是有各自的学问的,伦敦和纽约证券交易所历史悠久;纳斯达克则主要是做高科技公司的IPO;国内则是上海和深圳两个证券交易所,而香港也有个证券交易所。IPO之后,公司的发展就需要遵守很多制度了,比如财务报表公开等等。因为投资人也需要依赖财务报表来了解公司的运营状况,并对下一步的投资策略作出调整。

关于“天使投资”,名字倒是挺好听的,实际上与风险投资很相像。天使投资就是团队的第一笔投入的资金,有可能是几个团队的成员共同出的,也有可能是找天使投资基金出的。天使投资的风险比风险投资还要高很多,不过一般来说数额都是比较小的。

为什么要用股份制?股份制是一种利益共享,风险共担的公司运行方式。在施行股份制之后,如果公司运行状况转好,各个股东都会受益,当然,创始人的收入肯定不如独资的好;不过如果公司运行状况不好,那么大家也都是一起担着,不至于像独资企业那样可能老板本人都一蹶不振。再有就是公司的某些经营行为是必须要相当的资金投入的,比如互联网行业的购置服务器等,这个时候,往往依靠个人的投资难以实现,所以共同入股以促进公司的运营。再者,就是面对竞争了,一些推广计划中往往遇到竞争对手,这个时候,往往就是比拼双方实力的时候,如果技术不够优秀,就需要用更多的资金投入来袮补不足。最后,就是一个定理的问题了,对于绝大多数行业来说,投入越高,风险越小。无论是一个公司的哪个方向,投入更多的资金也就意味着更大的保险。比如,足够的资金可以聘请来第一流的CEO,可以招到最优秀的CTO,也可以用于更广泛的推广,招更多的工程师等等。

战略与战术。我最常听到想要创业的人说的就是:“等有了钱以后,如何如何”,典型的战略型人才。其实,战略型人才真的是遍地都是,那些根本不去创业的绝大多数人,也知道“有了钱以后”可以如何如何。但是问题难就难在,钱要哪里来?资金可以考虑风险投资,但是你必须要有强悍的发展能力,才可以让风险投资敢于对你投资。也就是说,在天使投资的阶段,就必须要做到最好,在没有拿到风险投资的时候,就已经奠定了成功的基础。

技术必要与否?最早也不知哪一位贤人说了一句:“做事情不能只关心技术”。结果就被一般俗人理解为“做事情不能关心技术”。于是后来俗人便口口相传,到了现在,已经成了流行语了。对于一个要告诉成长,短期内就要达到世界顶尖的公司,风险投资基金关注的事情,在各个阶段是不同的。不过对于一个根本不把技术人员放在眼里的公司,肯定是没戏。在第一轮风险投资的审核阶段,便可以认定一个公司的最初价值,这个时候,基本上产品网站用户寥寥无几。而即便是这个粗陋的产品,也是需要实现的,并不会从一个概念不经过任何技术人员立刻变成产品的。第一轮风险投资往往看中的就是创业团队的技术实力。这个技术实力,并不是技术这么简单,而是要做到顶尖,要巨牛无比的技术。到了第二轮风险投资时,风投基金就开始关注执行能力和未来的计划了。这个时候,最初的天才技术人员已经开始带领一些团队进行继续的产品开发了。第三轮风险投资时则更关注管理能力,包括创业团队每个人的管理能力,还有就是公司在整个行业中的位置。古语云:千里之行始于足下,想做把公司迅速做大,每一步都要走的踏实,不过想要跨过第一步的,往往还等不到第二步的时候也便摔了下去。

第二次互联网的热潮已经显出了降温的趋势,从很多经济的信息也看得出,风险投资基金对互联网的投入比例也在减少。这个时候想要依托风险投资迅速发展的公司更是需要积累足够的实力。不过,至少有一点是肯定的,就是一个优秀的人,无论做什么,都会更成功一些。而风险投资最看重的,一个公司的核心,其实也就是优秀的人。