上周我们发布了 phpy 项目的第一个版本,收到了大量 PHP 开发者的关注,许多开发者向我们提交问题、意见和建议、IssuesPull Request

经过一周的优化和修改完善,phpy 又得到了一些突破性进展。本文将详细解答大家广泛关注的问题,以及介绍第二个版本的变化:

运行原理

在进程内同时创建了 ZendVMCPython VM,直接在进程堆栈空间内使用 C 函数 互相调用, 开销只有 zval <-> PyObject 结构体转换,因此性能是非常高的。

phpy 基于 PHP 官方的 ZendAPIPython 官方的 Py C API 实现,没有其他外部的 C 库依赖。因此是可以实现跨平台的,LinuxWindowsmacOS 均可使用。

在 PHP-FPM 下使用

第一个版本中我们不建议在 PHP-FPM 环境下使用。在后续的测试发现在 PHP-FPM 环境下 import Python 包,仅第一次消耗比较多的时间,第二次直接使用了 Python sys.modules 中缓存的包,因此 phpy 是完全可以用于 PHP-FPMApache 等短生命周期环境下的。

甚至我们可以使用 phpy 使得一些对象在 PHP-FPM 下也能常驻内存。

$app = PyCore::import('app.user');
$storage = $app->storage;
if (!isset($storage['data'])) {
    $storage['data'] = uniqid();
    var_dump("no cache");
    $o = new stdClass();
    $o->hello = uniqid();
    $storage['obj'] = $o;
} else {
    var_dump("cached");
    var_dump(strval($storage['data']));
    var_dump($storage['obj']);
}

上面的代码将一个 Python 字典持久化了,在 PHP-FPM 下可以实现内存复用。

phpy 底层设置了内存安全边界,若 Python 中持久化了 PHP 对象,在请求结束后依然会销毁,并且将值设置为 NULL,不必担心出现内存错误

性能测试

压测脚本中创建了一个 PyDict ,分别读写 PHP 代码和 Python 代码执行 1000万次

  • PHP 版本PHP 8.2.3 (cli) (built: Mar 17 2023 15:06:57) (NTS)
  • Python 版本Python 3.11.5
  • 操作系统:Ubuntu 20.04
  • GCC 版本gcc version 9.4.0 (Ubuntu 9.4.0-1ubuntu1~20.04.2)
请注意此测试需要构造一个 1000 万元素的 HashTable,需要至少 2G 以上内存空间才可以运行
测试代码请参考 压力测试

结果对比

(base) htf@swoole-12:~/workspace/python-php/docs/benchmark$ php dict.php 
dict set: 4.663758 seconds
dict get: 3.980076 seconds
(base) htf@swoole-12:~/workspace/python-php/docs/benchmark$ php array.php 
array set: 1.578963 seconds
array get: 0.831129 seconds
(base) htf@swoole-12:~/workspace/python-php/docs/benchmark$ python dict.py 
dict set: 5.321664 seconds
dict get: 4.969081 seconds
(base) htf@swoole-12:~/workspace/python-php/docs/benchmark$

Python 测试为基准:

脚本名称SetGet
dict.php114%125%
array.php337%599%
  • phpyPHP 代码写入 PyDict 的性能比原生 Python14%,读取性能高 25%
  • PHP 写入 PHP Array 的性能比 Python 写入 Dict237%,读取高出了近 500%

异常捕获

最新版本支持了 PythonPHP 异常的融合,可以在 PHP 代码中捕获 Python 运行过程中触发的异常。

try {
    PyCore::import('not_exists');
} catch (PyError $e) {
    PyCore::print($e->error);
    PyCore::print($e->type);
    PyCore::print($e->value);
    PyCore::print($e->traceback);
}
  • 底层会自动将 $e->value 的字符串值设置为异常消息,可使用 $e->getMessage() 获取
  • PyError 未设置 $e->code 错误码,请勿使用

IDE 自动提示

phpy 提供了一个自动生成工具,可以生成 IDE 自动提示文件。使用方法:

cd phpy/tools
php gen-lib.php [Python 包名称]

例如 matplotlib.pyplot

  • 直接导入:PyCore::import('matplotlib.pyplot')
  • 生成提示文件:php tools/gen-lib.php matplotlib.pyplot

也可以配置 tools/gen-all-lib.php 批量生成多个包的提示文件。

安装依赖

composer require swoole/phpy

使用 IDE 提示

require dirname(__DIR__, 2) . '/vendor/autoload.php';
$plt = python\matplotlib\pyplot::import();

$x = new PyList([1, 2, 3, 4]);
$y = new PyList([30, 20, 50, 60]);
$plt->plot($x, $y);
$plt->show();

编译参数

第一个版本中我们使用了硬编码的 Python 开发目录,新版本可以使用编译参数来指定,并且底层还会自动识别 Python 版本。

现在 phpy 最低支持 Python 3.8PHP 8.1,另外我们增加了一个 Dockerfile 可以参考此文件来构建 phpy 的环境。

--with-python-dir

指定 Python 的安装路径,例如 /usr/bin/python 应该设置为 --with-python-dir=/usr
若使用 conda 安装 Python,应设置为 /opt/anaconda3

--with-python-version

指定 Python 的版本,例如 3.103.113.12,默认将使用 $python-dir/bin/python -V 来获取版本。

动态链接库问题

导入库时发生动态链接库错误,原因可能是 LD 路径错误导致,可设置环境变量指定 Python C 模块 动态库路径。

export LD_LIBRARY_PATH=/opt/anaconda3/lib
php plot.php

这种方式仅对当前的 bash 会话有效,不会影响全局,更加安全。不要直接修改 /etc/ld.so.conf.d/*.conf 增加 /opt/anaconda3/lib,这可能会导致 libc 库冲突,可能会影响操作系统其他程序的正常运行。

支持全部 Python 内置方法

在第一个版本中,我们使用了 C 代码实现了一部分内置函数,第二个版本中我们直接设置了 PyCore::__callStatic() 魔术方法,对于 PyCore 静态方法调用会自动调用 Pythonbuiltins 模块对应的方法。支持了全部 Python 内置方法。

可参考 Built-in Functions 了解更多内置方法的使用

甚至我们可以使用 evalexecPHP 中执行 Python 代码。

$pycode = <<<PYCODE
square = {
    f'{prefix}{i}': i**2 for i in range(n)
}
PYCODE;

$globals = new PyDict([
    'n' => 10,
    'prefix' => 'square_',
]);

PyCore::exec($pycode, $globals);
$this->assertEquals(64, $globals['square']['square_8']);
$this->assertEquals(16, $globals['square']['square_4']);

迭代器支持

现在可以使用迭代器来遍历 Python 对象,可以完美支持 Pythonyield 生成器语法。

$iter = PyCore::iter($uname);
$this->assertTrue($iter instanceof PyIter);

$list = [];
while ($next = PyCore::next($iter)) {
    $list[] = PyCore::scalar($next);
}

PHP 单元测试

phpy 项目拥有完整的单测覆盖来保证稳定性,安装成功后可使用 composer test 或者 phpunit脚本进行测试。

Python 单元测试

phpy使用了pytest工具编写了Python调用PHP API的用例。可使用 pytest -v tests/ 来测试模块可用性。

更多例子

Numpy 科学计算

$np = PyCore::import('numpy');
$rs = $np->floor($np->random->random([3, 4])->__mul__(10));
PyCore::print($rs);

matplotlib.pyplot 数学绘图

$plt = PyCore::import('matplotlib.pyplot');

$x = new PyList([1, 2, 3, 4]);
$y = new PyList([30, 20, 50, 60]);
$plt->plot($x, $y);
$plt->show();

微信交流群


韩天峰
7.9k 声望11.1k 粉丝

Swoole 开源项目创始人