上周我们发布了 phpy
项目的第一个版本,收到了大量 PHP
开发者的关注,许多开发者向我们提交问题、意见和建议、Issues
和 Pull Request
。
经过一周的优化和修改完善,phpy
又得到了一些突破性进展。本文将详细解答大家广泛关注的问题,以及介绍第二个版本的变化:
运行原理
在进程内同时创建了 ZendVM
和 CPython VM
,直接在进程堆栈空间内使用 C 函数
互相调用, 开销只有 zval <-> PyObject
结构体转换,因此性能是非常高的。
phpy
基于 PHP
官方的 ZendAPI
和 Python
官方的 Py C API
实现,没有其他外部的 C
库依赖。因此是可以实现跨平台的,Linux
、Windows
、macOS
均可使用。
在 PHP-FPM 下使用
第一个版本中我们不建议在 PHP-FPM
环境下使用。在后续的测试发现在 PHP-FPM
环境下 import Python
包,仅第一次消耗比较多的时间,第二次直接使用了 Python sys.modules
中缓存的包,因此 phpy
是完全可以用于 PHP-FPM
或 Apache
等短生命周期环境下的。
甚至我们可以使用 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
测试为基准:
脚本名称 | Set | Get |
---|---|---|
dict.php | 114% | 125% |
array.php | 337% | 599% |
phpy
以PHP
代码写入PyDict
的性能比原生Python
高14%
,读取性能高25%
PHP
写入PHP Array
的性能比Python 写入 Dict
高237%
,读取高出了近500%
异常捕获
最新版本支持了 Python
和 PHP
异常的融合,可以在 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.8
和 PHP 8.1
,另外我们增加了一个 Dockerfile 可以参考此文件来构建 phpy
的环境。
--with-python-dir
指定 Python
的安装路径,例如 /usr/bin/python
应该设置为 --with-python-dir=/usr
。
若使用 conda
安装 Python
,应设置为 /opt/anaconda3
--with-python-version
指定 Python
的版本,例如 3.10
、3.11
、3.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
静态方法调用会自动调用 Python
的 builtins
模块对应的方法。支持了全部 Python
内置方法。
可参考 Built-in Functions 了解更多内置方法的使用
甚至我们可以使用 eval
和 exec
在 PHP
中执行 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
对象,可以完美支持 Python
的 yield
生成器语法。
$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();
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。