TypePHP 可以在语言层面直接调用 Python 模块、函数和对象。两个运行时位于同一个进程中,参数和返回值通过 phpy 转换,不需要 JSON、RPC 或 Python 子进程。
Python 互调用是可选的扩展级特性。不使用 Python 语法的 TypePHP 程序不依赖 phpy;使用该特性时,需要在程序的 PHP 运行环境中加载 phpy 扩展。
首先安装 CPython、开发头文件和 phpy。phpy 支持 Linux、macOS 和 Windows,当前要求 Python 3.10 或更高版本、PHP 8.1 或更高版本。以源码构建为例:
git clone https://github.com/swoole/phpy.git
cd phpy
phpize
./configure --enable-phpy --with-python-config=/usr/bin/python3-config
make -j
sudo make install在 TypePHP 程序使用的 php.ini 中加载扩展:
extension=phpy验证扩展和 Python 运行时:
php --ri phpy
/usr/bin/python3 --version第三方 Python 包必须安装到 phpy 所使用的同一个 Python 环境中。例如:
/usr/bin/python3 -m pip install numpyphpy 与 TypePHP/PHP 的 Zend ABI 必须匹配。不要混用针对不同 PHP 版本、ZTS/NTS 模式或 Debug/Release ABI 构建的扩展,否则可能在程序启动或对象析构时崩溃。
TypePHP 不直接链接 libphpy.so。编译器只生成对 Zend class、method 和 object API 的动态调用,因此没有使用 Python 的代码不会增加运行时依赖。若程序实际执行了 Python 表达式但 phpy 未加载,会抛出普通 PHP Error;Python 模块不存在或 Python 调用失败时,则抛出 PyError。
<?php
use Python\math;
function main(): void
{
$result = math\sqrt(81);
echo $result->toValue()->toFloat(), "\n";
}编译并运行:
tpc hello.php
./hello输出:
9
math\sqrt() 的返回值默认仍是 PyObject。toValue() 明确地将它转换到 TypePHP/PHP 类型系统,再由 toFloat() 得到 float。
使用 use python\module 导入模块:
use python\sys;
use Python\numpy as np;
use python\numpy\linalg as linalg;这分别对应 Python 的:
import sys
import numpy as np
import numpy.linalg as linalgpython 根命名空间不区分大小写,因此 python、Python 和 PYTHON 都合法。模块路径、成员名、方法名和关键字参数仍严格区分大小写:
python\len([1, 2, 3]); // 正确
Python\len([1, 2, 3]); // 正确
python\Len([1, 2, 3]); // 错误:Python 中不存在 Lenuse python\... 只建立编译期模块别名,模块在第一次实际访问时才导入。仅声明但从未使用的模块不会调用 Python,也不会因为模块未安装而报错:
use python\module_that_is_not_installed;
function main(): void
{
echo "This program does not import the module.\n";
}use 本身完全遵循 PHP 的命名空间与别名规则,TypePHP 不为 Python 改写 PHP 的名称解析。在全局 namespace 中可以直接写 python\math\sqrt();位于 namespace App 时,python\math\sqrt() 会被 PHP 解析为 App\python\math\sqrt(),因此必须导入或使用全限定名称:
namespace App;
use python\math;
$a = math\sqrt(16); // 导入后的别名
$b = \python\math\sqrt(25); // 全限定名称,无需 usepython 是 TypePHP 的特殊根命名空间,不能用来声明普通 PHP 命名空间。模块别名也不能与当前文件中的其他 use 符号冲突。
也可以使用 PHP 原生的函数和常量导入语法,并使用 as:
namespace App;
use function python\len;
use function python\math\sqrt as pySqrt;
use const python\math\pi as pyPi;
len([1, 2, 3]);
$root = pySqrt(16);
$pi = pyPi;Python 符号仍区分大小写;PHP 的 alias 解析则继续遵循 PHP 自身规则。
Python 模块中的 callable 使用 PHP 命名空间函数语法调用:
use Python\numpy as np;
$array = np\array([1, 2, 3]);
$zeros = np\zeros([2, 3]);np\array() 可能是函数、Python class 或实现了 __call__ 的对象。TypePHP 不猜测成员种类,可调用性由 Python 在运行时判断。
模块变量使用 PHP 命名空间常量语法读取:
use Python\math;
use Python\sys;
$pi = math\pi;
$path = sys\path;不要写成 math::pi 或 math::$pi,它们属于 PHP class member 语法。Python 模块在 TypePHP 中映射为命名空间,成员读取仍由 Python 在运行时完成。
模块变量的值同样是 PyObject。TypePHP 只提供读取,不支持通过 namespace 语法覆盖或删除模块变量:
$path = sys\path; // 支持
sys\path = $newPath; // 不支持
unset(sys\path); // 不支持确实需要修改 Python 模块状态时,可以显式调用 Python 的 setattr() / delattr();这属于应用主动执行的 Python 操作。
模块按完整名称缓存。同一个模块在多个文件中使用不同别名时,底层仍遵循 Python 的 sys.modules 导入语义。
通过 python\name() 调用 Python builtin:
$length = python\len([1, 2, 3]);
$power = python\pow(2, 10);
python\print('Hello from Python');常用 Python 代理对象可以直接构造:
$list = python\list([1, 2, 3]); // PyList
$dict = python\dict(['answer' => 42]); // PyDict
$tuple = python\tuple([1, 2]); // PyTuple
$set = python\set([1, 2]); // PySet
$str = python\str(123); // PyStr
$integer = python\int('42'); // PyObject
$bytes = python\bytes("binary"); // PyObject其中容器构造是 phpy Facade 的语法糖,例如:
$a = python\list([1, 2, 3]);
$b = new PyList([1, 2, 3]);两者具有相同的运行时语义。特别是 python\dict($phpArray) 按 PHP 数组的 key/value 构造 PyDict,并不是直接执行 CPython 的 dict(iterable)。
Python builtin 的一级可调用语法目前不受支持。需要将 Python callable 保存或传递时,可以先从模块属性或对象属性取得对应的 PyObject。
Python 返回值使用 phpy 已有的代理类表示。无法静态确定具体类型时统一为 PyObject,不会引入另一套 python\Object 类名。
$name = $object->name;
$object->name = 'TypePHP';
$result = $object->greet('hello', suffix: '!');
unset($object->name);这些操作分别调用 Python 的属性读取、setattr、call 和 delattr 协议。Python 方法支持命名参数,参数名称区分大小写。
$last = $list[-1];
$list[-1] = 42;
unset($list[-2]);
if (isset($dict['name'])) {
echo $dict['name'];
}list 和 tuple 支持 Python 负索引。isset() 保持 PHP 语义:key/index 不存在,或者对应值为 Python None 时返回 false。除 KeyError 和 IndexError 之外的 Python 异常不会被吞掉。
foreach ($pythonIterable as $index => $value) {
echo $index, ': ', $value, "\n";
}通用 Python iterator 的 key 是从 0 开始的迭代序号。PyDict 使用字典自身的 key/value。迭代期间发生的 Python 异常会作为 PyError 继续传播。
$result = $pythonCallable($left, right: 42);
$args = [1, 2, 3];
$result = $pythonCallable(...$args);不可调用的 Python 对象会抛出包含 Python TypeError 的 PyError。
进入 Python 函数、方法、构造器或运算符边界时,TypePHP 值自动转换为 Python 值:
| TypePHP 值 | Python 值 |
|---|---|
null |
None |
bool |
bool |
int |
int |
float |
float |
string |
str,字符串必须是合法 UTF-8 |
| list array | list |
| map array | dict |
| 空数组 | list |
PyObject 及其子类 |
保留原 Python 对象,不复制内容 |
| TypePHP callable | 可由 Python 同步调用的 callable proxy |
数组会递归深拷贝。递归数组、循环容器、过深嵌套和无效 UTF-8 会抛出 PyError,不会无限递归或导致进程崩溃。
所有参数严格按照源码从左到右求值,并且只求值一次:
$result = python\pow(mark(2), mark(3));如果同一个数组需要在循环中频繁传给 Python,建议提前转换并复用代理对象:
$pyItems = python\list($items);
for ($i = 0; $i < 1000; $i++) {
processor::consume($pyItems); // 只传递 Python 对象引用
}避免每次调用都直接传 $items,否则每次跨越边界都会重新深拷贝。二进制数据应使用 python\bytes();普通 TypePHP string 默认转换为 Python str。
Python 函数、方法、构造调用和运算结果默认保持为 PyObject 或其具体代理子类。即使 Python 返回 int、float、bool 或 str,TypePHP 也不会根据运行时类型自动改变变量类型。
使用 PyObject::toValue() 显式进入 TypePHP/PHP 类型系统:
$pyValue = python\int(42);
$plain = $pyValue->toValue();
$integer = $pyValue->toValue()->toInt();
$float = $pyValue->toValue()->toFloat();
$boolean = $pyValue->toValue()->toBool();
$string = $pyValue->toValue()->toString();
$array = python\list([1, 2, 3])->toArray();函数式写法 python\scalar($value) 与 phpy 的 PyCore::scalar($value) 等价,可用于兼容已有代码:
$integer = python\scalar($pyValue)->toInt();toValue() 与 python\scalar()、PyCore::scalar() 使用同一套转换规则。转换后的结果是普通 TypePHP 值,后续运算不再使用 Python protocol。
toArray() 专门转换 Python list、set、tuple、dict 和迭代器对象,并递归深拷贝其中的元素;不支持的 Python 类型返回空数组。迭代器会被消费,重复调用可能得到空数组。phpy 在 PyObject 上提供具体的 toArray() 方法;它同时也是 TypePHP 关键词方法。receiver 是静态可知的 PyObject 子类时,编译器会直接调用 phpy 方法,否则回退到通用转换路径。toValue() 只是 PyObject 的普通方法,不是 TypePHP 关键词方法。
toString() 仍是 TypePHP 关键词方法,会使用 PyObject::__toString();无需在 phpy 中额外提供同名转换方法。若要转换为其他 PHP 标量,应先调用 toValue(),例如 $pyObject->toValue()->toInt()。
只要表达式的一侧静态类型是 PyObject 或其子类,TypePHP 就使用 Python 的完整运算符协议。另一侧的普通 TypePHP 值会先转换为 Python 对象:
$seven = python\int(7);
$three = python\int(3);
$sum = $seven + $three;
$product = $seven * 10;
$reflected = 10 + $seven;
$quotient = $seven / 2; // Python true division支持的协议包括:
- 算术、幂和位运算:
+ - * / % ** << >> & | ^。 - 一元运算:
+ - ~。 - 比较:
== != < <= > >=。 - identity:
===和!==。 - 条件真假值、
!、&&、||和xor。 - 对变量、对象属性和下标执行复合赋值,例如
+=、*=、<<=。
/ 使用 Python true division,不是 floor division。TypePHP 没有 Python 的 // 运算符;需要 floor division 时显式调用相应 Python 函数。
== 使用 Python 值相等协议;=== 使用 Python object identity:
$list = python\list([1]);
$alias = $list;
var_dump($list === $alias); // true
var_dump($list === python\list([1])); // false复合赋值使用 Python in-place protocol,并用协议返回的对象更新左值,因此对可变和不可变 Python 类型都能得到正确结果。
TypePHP AOT 保留源码 AST,因此:
-$value; // Python operator.neg(value)
$value * -1; // Python operator.mul(value, -1)两者会调用不同的 Python protocol,行为可以不同。
通过 ZendVM 动态执行的普通 PHP 代码是一个例外。Zend 会把 -$value 和 +$value 编译成乘以 -1 和 1,phpy 的 opcode handler 无法恢复原始源码意图。因此动态代码中的 -$value 保持 $value * -1 的行为,+$value 保持 $value * 1 的行为。
对于普通 Python int 和常规 float,结果通常相同;自定义 Python 类型可以分别实现 __neg__()、__pos__() 和 __mul__(),此时 AOT 与动态代码可能不同。TypePHP 不通过全局 AST hook 改写普通 PHP 代码。
TypePHP 函数、闭包和可调用对象可以作为 Python 参数,并由 Python 在当前调用链中同步回调:
$values = python\list([1, 2, 3]);
$mapped = python\map(
fn (int $value): int => $value * 2,
$values,
);
$sum = python\sum($mapped)->toValue()->toInt();
echo $sum, "\n"; // 12Python 传给回调的关键字参数会按名称绑定到 TypePHP callable 的参数。回调是同步的,只能在 TypePHP 主动发起的 Python 调用关系中使用;TypePHP 不会向 Python 注册可独立 import 的函数、类或 module。
Python module 不存在、成员不存在、参数错误、类型错误和用户代码异常统一映射为 PyError:
try {
python\len();
} catch (PyError $error) {
echo $error->getMessage(), "\n";
// 原始 Python 异常对象,均为可选 PyObject 属性。
$type = $error->type;
$value = $error->value;
$traceback = $error->traceback;
}PyError 继承自 PHP Exception,并保留 type、value、error 和 traceback 等 Python 对象。普通 PHP 错误仍使用 PHP 异常体系,例如未加载 phpy 时解析不到 PyCore 会抛出 Error。
跨 VM 异常不会被静默转换为 null。处理异常后可以继续进行 Python 调用,phpy 会清理 CPython 的 pending error state。
TypePHP 没有重新实现 Python VM,也没有创建第二套 Python 对象类。下列写法可以混用:
use Python\os;
$module1 = PyCore::import('os');
$name1 = $module1->name;
$name2 = os\name;
$list1 = new PyList([1, 2, 3]);
$list2 = python\list([1, 2, 3]);use python\module、python\name() 和 Python 运算符是 TypePHP 的编译期语法糖;CPython 初始化、GIL、引用计数、对象代理、转换和异常全部由 phpy 负责。TypePHP 不 include phpy 头文件,也不生成对 phpy C++ 符号的直接调用。
- 不支持 Python
threading。 - 不支持
asyncio。 - 不支持 CPython subinterpreter。
- 不把 TypePHP 编译成 Python extension,也不支持从 Python 主动 import TypePHP 程序。
- 不提供 TypePHP 函数或类的 Python 导出注解。
- 不编译 Python 源码;Python 模块仍由 CPython 在运行时加载。
- 不支持
from package import *语法。 - 不支持 Python builtin 的一级可调用语法。
- Python 符号是否存在通常只能在运行时确定。
- Python 互调用目前不适用于 TypePHP WASM 目标。
- 动态 PHP 代码的一元正负号存在前述 protocol 差异。
TypePHP 的目标是让应用高效、可靠地调用 Python 包,而不是在 PHP 中完整实现 Python 语法或异步运行时。
开发阶段还可以使用:
- 生成 Python IDE 自动提示:为 builtin 和指定模块生成仅供编辑器索引的声明。
- Python 代码转 TypePHP:把受支持的 Python AST 机械转换为可继续审查和修改的 TypePHP 源码。