有几个原因使您可能想用 C 扩展 Zope。最可能的是您有一个已能帮您做些事的现成的 C 库,但是您对把它转换成 Python 却不感兴趣。此外,由于 Python 是解释性语言,所以任何被大量调用的 Python 代码都将降低您的速度。因此,即使您已经用 Python 写了一些扩展,您仍然要考虑把其中最常被调用的部分改用 C 来写。不论哪种方式,扩展 Zope 都是从扩展 Python 开始。此外,扩展 Python 会给您带来其它的好处,因为您的代码将可以从任何 Python 脚本访问,而不只是从 Zope。这里唯一要提醒的是在写本文的时候,Python 的当前版本是 2.1,但是 Zope 仍然只能和 Python 1.5.2 一起运行。对 C 扩展来说,两个版本并没有什么变化,但如果您有兴趣对您的库进行 Python 包装,又想让它们都能在 Zope 下工作,您就得注意不要使用任何比 1.5.2 更新的东西。
Zope 是什么?
Zope 代表“Z Object Publishing Environment(Z 对象发布环境)”,它是用 Python 实现的应用程序服务器。“太棒了,”您说,“但应用程序服务器的确切含义是什么呢?”应用程序服务器就是一个长期运行的进程,它为“活动的内容”提供服务。Web 服务器在运行期间调用应用程序服务器来构建页面。
扩展 Python:有趣又有益
想扩展 Zope,您首先要扩展 Python。虽然扩展 Python 不像“脑外科手术”那样复杂,但也不像“在公园中散步”那样悠闲。有两个基本组件用于 Python 扩展。第一个显然是 C 代码。我将马上探讨它。 另一个部分是 安装文件。安装文件通过提供模块名称、模块的 C 代码的位置和您可能需要的所有编译器标志来描述模块。该文件被预处理,以创建 makefile(在 UNIX 上)或 MSVC++ 工程文件(MSVC++ project file,在 Windows 上)。先说一下 ― Windows 上的 Python 事实上是用 Microsoft 编译器编译的。Python.org 的人也推荐用 MSVC++ 编译扩展。显然,您应该能够成功说服 GNU 的编译者们,但我本人还没试过。
无论如何,还是让我们来定义一个叫做‘foo'的模块吧。‘foo'模块会有一个叫做‘bar'的函数。当我们要使用时,我们可以用 import foo; 来把这个函数导入到 Python 脚本中,就跟导入任何模块一样。安装文件非常简单:
清单 1. 一个典型的安装文件
# You can include comment lines. The *shared* directive indicates # that the following module(s) are to be compiled and linked for # dynamic loading as opposed to static: .so on Unix, .dll on Windows. *shared* # Then you can use the variables later using the $(variable) syntax # that 'make' uses. This next line defines our module and tells # Python where its source code is. foo foomain.c
编写代码
那么我们实际上该怎样写 Python 知道如何使用的代码呢,您问? foomain.c (当然,您可以随意命名它)文件包含三项内容:一个方法表,一个初始化函数和其余的代码。方法表简单地将函数名与函数联系起来,并告知 Python 各个函数所使用的参数传递机制(您可以选择使用一般的位置参数列表或位置参数和关键词参数的混合列表)。Python 在模块装入时调用初始化函数。初始化函数将完成模块所要求的所有初始化操作,但更重要的是,它还把一个指向方法表的指针传回给 Python。
那我们就来看看我们的小型 foo 模块的 C 代码。
清单 2. 一个典型的 Python 扩展模块
#include <Python.h> /* Define the method table. */ static PyObject *foo_bar(PyObject *self, PyObject *args); static PyMethodDef FooMethods[] = { {"bar", foo_bar, METH_VARARGS}, {NULL, NULL} }; /* Here's the initialization function. We don't need to do anything for our own needs, but Python needs that method table. */ void initfoo() { (void) Py_InitModule("foo", FooMethods); } /* Finally, let's do something ... involved ... as an example function. */ static PyObject *foo_bar(PyObject *self, PyObject *args) { char *string; int len; if (!PyArg_ParseTuple(args, "s", &string)) return NULL; len = strlen(string); return Py_BuildValue("i", len); }
深入研究
我们来看会儿这些代码。首先,请注意您必须包含 Python.h 。除非您已在包含路径(include path)中设置了该文件的路径,否则您可能需要在安装文件中包含 -I 标志以指向该文件。
初始化函数必须命名为 init ,在我们的例子中是 initfoo 。初始化函数的名称,毫无疑问,是 Python 在装入模块时所知道的关于模块的全部信息,这也是初始化函数的名称如此死板的原因。顺便说一下,初始化函数必须是文件中唯一未被声明为 static 的全局标识符。这对静态链接比对动态链接更重要,因为非 static 标识符将是全局可见的。对动态链接来说,这不是一个很大的问题,但如果您打算在编译期间链接所有东西,又没有把所有可以声明为 static 的东西声明为 static ,那么您很可能就会碰到名称冲突的问题。
现在我们来观察实际的代码,看看参数是怎样被处理的,返回值又是怎样被传递的。当然,一切都是 PyObject ― Python 堆上的对象。您从参数中得到的是一个对“this”对象的引用(this 用于对象方法,对类似 bar() 这样的无参数的老式函数来说是 NULL)和一个存储在 args 中的参数元组。您用 PyArg_ParseTuple 找回您的参数,然后用 Py_BuildValue 把结果传回去。这些函数(还有更多)都归档在 Python 文档的“Python/C API”部分中。不幸的是,没有按名称排列的简单的函数清单,文档是按主题排列的。
另请注意,函数在出错的情况下返回 NULL。返回 NULL 表示出错了;如果想让 Python 做得更好,您应该抛出异常。我会指点您去查阅关于如何做这件事的文档。
编译扩展
现在剩下的全部问题是编译模块。您可以通过两种方式进行。第一种是按照文档中的指导,运行 make -f Makefile.pre.in boot ,这样将会使用您的 Setup 来编译一个 Makefile。然后您就用该 Makefile 编译您的工程。这种方式只适用于 UNIX。对 Windows 来说,存在一个叫“compile.py”的脚本(请参阅本文后面的 参考资料)。原始脚本很难找到;我从一个邮件列表中找到了一个来自 Robin Dunn(wxPython 的幕后工作者)的被大量改动了的副本。这个脚本能在 UNIX 和 Windows 上工作;在 Windows 上,它将从您的 Setup 开始编译 MSVC++ 工程文件。
要进行编译,您必须使包含的文件和库都可用。Python 的标准 Zope 安装没有包含这些文件,因此您需要从 www.python.org(请参阅 参考资料)安装 Python 的常规安装。在 Windows 上,您还必须从源代码安装的 PC 目录中获取 config.h 文件;它是 UNIX 安装为您编译的 config.h 的手工版。因此,在 UNIX 上,您应该已经拥有它了。
一旦这些都完成后,您就会得到一个以“.pyd”为扩展名的文件。把这个文件放到 Python 安装目录下的“lib”目录(在 Zope 下,Python 位于“bin”目录,因此您的扩展得结束于“bin/lib”目录,奇怪吧。)然后您就可以调用它了,就像调用任何源生的 Python 模块一样。
>>> import foo; >>> foo.bar ("This is a test"); 14
做到这里时,我的第一个问题是问自己该如何用 C 定义从 Python 中可见的 类。事实上,我可能问了一个错误的问题。在我已研究的示例中,特定于 Python 的一切都只 用 Python 来完成,也都只调用从您的扩展中导出的 C 函数。
把它带到 Zope 中去
一旦完成了您的 Python 扩展,下一步就是使 Zope 能和它一起工作。您有几种方式可以选择,但在一定程度上,您希望您的扩展以什么方式与 Zope 一起工作将首先影响到您编译扩展的方式。从 Zope 内使用 Python(以及用 C 所做的扩展)代码的基本方式是:
当然,您自己的应用程序可以使用这些方式的组合。
创建外部方法
从 Zope 调用 Python 的最简单的方式是把您的 Python 代码做成 外部方法。外部方法是被放到 Zope 安装目录下的“Extensions”目录中的 Python 函数。一旦那里有了这样一个 Python 文件,您就可以转到任意文件夹,选择“添加外部方法”,并添加调用要使用的函数的变量。然后您就可以往该文件夹中显示调用结果的任意页添加 DTML 字段。我们来看一个使用了上面所定义的 Python 扩展 ― foo.bar ― 的简单示例。
首先,来看扩展本身:我们把它放到一个例如叫 foo.pyd 的文件中。记住,这个文件位于 Zope 下的 Extensions 目录。为了能够顺利进行,当然,我们在上面创建的 foo.pyd 必须在位于 bin/lib 的 Python 库中。一个出于这个目的的、简单的包看起来可能像这样:
清单 3. 一个简单的外部方法(文件:Extensions/foo.py)
import foo def bar(self,arg): """A simple external method.""" return 'Arg length: %d' % foo.bar(arg)
很简单,不是吗?它定义了一个可以用 Zope 管理界面附加到任意文件夹的外部方法“bar”。要从该文件夹中的任何页中调用我们的扩展,我们只需简单地插入一个 DTML 变量引用,如下所示:
<dtml-var bar('This is a test')>
当用户查看我们的页时,DTML 字段将被文本“Arg length: 14”代替。我们就这样用 C 扩展了 Zope。
Zope 脚本:Cliff Notes 版
Zope 脚本是 Python 2.3 的一个想用来代替外部方法的新功能。外部方法能做到的,它都能做到,而且它能和安全性及管理系统更好地集成,在集成方面提供更多的灵活性,它还有很多对 Zope API 中公开的全部 Zope 功能的访问。
一个脚本基本上就是一个短小的 Python 程序。它可以定义类或函数,但不是必须的。它被作为对象安装在 Zope 文件夹中,然后就可以把它当作 DTML 变量或调用(就像一个外部方法)来调用或者“从 Web 中”(在 Zope 中的意思就是它将被当作页来调用)调用它。当然,这意味着脚本可以像 CGI 程序那样生成对表单提交的响应,但却没有 CGI 的开销。确实是一个很棒的功能。此外,脚本有权访问被调用者或调用者对象(通过“context”对象)、对象所在的文件夹(通过“container”对象)和其他一些零碎信息。要获得更多关于脚本的知识,请参阅 Zope 手册(请参阅 参考资料)中的“高级 Zope 脚本编制(Advanced Zope Scripting)”那一章。
您可能会错误地认为可以直接从脚本简单地导入 foo 并使用 foo.bar(我知道我确实犯过这种错误)。但事实并非如此。由于安全性限制,只有 Product 可以被导入,而不是什么模块都可以。一般而言,Zope 的设计者们认为任何脚本编制都需要访问文件系统,既然脚本对象是由 Web 使用 Zope 管理界面来管理,所以它们不是完全可信的。所以我打算就此打住,不给您展示示例脚本了,而是来讨论 Product 和基础类。
专注于 Product
Product 是扩展 Zope 的强大工具方法。从安装目录的级别来看,Product 就是位于 Zope 目录下的“lib/python/Products”目录中的一个目录。在您自己的 Zope 安装目录中,您可以看到很多 product 示例,但本质上,最小的 Product 只由位于该目录的两个文件组成:一个可任意命名的代码文件和一个 Zope 在启动时调用来初始化 Product 的称为 __init__.py 的文件。(请注意:Zope 只在启动时读取 Product 文件,这意味着为了测试,您必须能够停止和重新启动 Zope 进程)。本文只是尽量多提供一些您能通过使用 Zope Product 做到的事的提示。
要知道的是 Product 封装了一个或多个可从 ZClass、脚本或直接从 Web 上的 URL 使用的类。(当然,在最后一种情况下,Product 的实例被当作文件夹看待;那么 URL 的最后部分指定了将被调用的方法,该方法返回任意的 HTML。)您不必一定要把 Product 当作“可添加的”对象来对待,虽然这是它的主要目的。要看一个优秀的、现实存在的示例,可以去看 ZCatalog 实现,它是标准 Zope 分发的一部分。那里您可以在 __init__.py 中看到一个非常简单的安装脚本,可以在 ZCatalog.py 中看到 ZCatalog 类,该类提供了很多发布方法。请注意 Zope 采用一种奇怪的约定来确定哪些方法可以通过 Web 访问 ― 如果一个方法包含有一个 doc 字符串,那么该方法可通过 Web 访问;否则,就被认为是私有的。
无论如何,我们还是来看一个使用了 C 模块(我们在上面定义了它)的非常简单的 Product。首先来看非常简单的 __init__.py;请注意它只做了一件事,即告诉 Zope 我们正在安装的类的名称。更复杂的初始化脚本能做 更多的事,包括声明由服务器维护的全局变量以及设置访问权限等等。欲了解更多详细信息,请参阅在线文档中的 Zope 开发者指南,也请研究您的 Zope 安装目录中现成的 Product。您或许已经猜到了,我们的示例 Product 被称为“Foo”。这样您就将在 lib/python/Products 目录下创建一个 Foo 子目录。
清单 4. 基本的 Product 初始化脚本
import Foo def initialize(context): context.registerClass( Foo.Foo, permission='Add Foo', constructors=Foo.manage_addFoo )
现在请注意这个初始化脚本不仅导入了那个类,使它可被 Zope 的其它部件访问,而且还将该类注册成具有“可添加性”。 context.registerClass 调用通过首先命名我们所导入的类,然后指定可被用于添加实例的方法名称(这个方法必须显示一个管理页面,且该方法将自动与 Zope 管理界面集成)的名称来完成这项工作。酷。
我们来小结一下这个短小、简单的 Product。它会把我们的 foo.bar 函数公开给脚本和 ZClass,并且还有一个作为“可添加的”对象的小接口,这就是全部内容。
清单 5. 一个简单的 Zope Product
import foo class Foo(SimpleItem.Item): "A Foo Product" meta_type = 'foo' def bar(self, string): return foo.bar(string) def __init__(self, id): "Initialize an instance" self.id = id def index_html(self): "Basic view of object" return ' My id is %s and its length is %d. ' % (self.id, foo.bar(self.id)) def manage_addFoo(self, RESPONSE): "Management handler to add an instance to a folder." self._setObject('Foo_id', Foo('Foo_id')) RESPONSE.redirect('index_html')
这只是一个最简单的 Product。不能绝对地说它是可能的 Product 中最小的一个,但已经很接近了。不过,它确实说明了 Product 的一些关键特征。首先,请注意“index_html”方法:它被调用来显示一个对象实例,这是通过构建 HTML 完成的。它实际上是一个页面。 manage_addFoo 方法是 Zope 对象管理的接口;我们在上面的 __init__.py 中引用了它。“__init__”方法初始化对象;实际上它 必须做的全部工作就是记录实例的唯一标识符。
这个微型的 Product 不和 Zope 安全性进行交互操作。它不做很多管理工作。它没有交互功能。所以您可以给它添加很多东西(甚至连很有用的功能它也没有)。我希望这对您是一个很好的开始。
以后该做什么
对 Zope Product 的简单介绍已经告诉您如何把 C 语言函数从 C 代码变为 Zope 中可用的。要学会怎么写 Product,您还得阅读更多文档(其中有很多仍在完善之中),坦率地说,还要研究已有的 Product,看看它们是怎么做的。Zope 模型有很强大的功能和很大的灵活性,它们都很值得探究。
我目前正在做集成 C 和 Zope 的大工程:集成我的工作流工具包(workflow toolkit)。在本文发表之前,我希望能看到它的雏形。它已被列在下面的参考资料中,去看看吧;到您阅读本文时,应该已经能够从中找到一个扩展示例。祝我好运。