2025, Nov 28 18:02

Определяем тип py::object в pybind11: complex и вызываемость без eval

Как в pybind11 определить тип py::object из Python в C++: проверяем complex и callable через builtins и isinstance, без eval. Примеры кода и рекомендации.

Связывая C++ и Python через pybind11, время от времени сталкиваешься с на вид простой задачей: определить динамический тип py::object. Базовые варианты — int, float, str, list — обрабатываются без труда, но комплексные числа и дескрипторы функций часто вынуждают идти на неудобные ухищрения. Хорошая новость: есть прямой, аккуратный способ сделать это без eval и без вспомогательных буферов.

Кратко о проблеме

Предположим, нужно преобразовать py::object (пришедший из Python) в нативный тип C++, который повторяет поведение Python. Для комплексных чисел и функциональных объектов «быстрый и грязный» путь — проскочить через фрагменты Python‑кода с помощью eval. Ниже — минимальный пример такого подхода.

bool isMaybeComplex(py::object any)
{
    int idx = stash_src(any); // сохранить в общем буфере, доступном обеим сторонам
    std::string test_expr;
    test_expr = "isinstance(bridge.pull_src("; // bridge.pull_src(idx) извлекает объект обратно
    test_expr += std::to_string(idx);
    test_expr += "), complex)";
    std::string eval_expr;
    eval_expr = "eval('";
    eval_expr += test_expr;
    eval_expr += "')";
    py::object py_builtins = py::module_::import("builtins");
    py::object fn_eval = py_builtins.attr("eval");
    py::object verdict = fn_eval(eval_expr);
    return py::isinstance<py::bool_>(verdict) && py::cast<py::bool_>(verdict);
}

И ту же идею применяют при проверке, вызываем ли объект:

test_expr = "callable(bridge.pull_src(";
test_expr += std::to_string(idx);
test_expr += "))";

Что происходит на самом деле

Подход выше выполняет «круговой рейс» через Python: динамически строит выражение и вычисляет его на лету. Он работает, но получается хрупко, медленно и непонятно. На деле нужно спросить у Python: «этот объект — экземпляр complex?» и «этот объект можно вызывать?». Эти проверки уже есть в встроенных механизмах Python. Eval не нужен — достаточно получить нужные функции напрямую из C++ через pybind11 и вызвать их.

Аккуратное решение: обращаться к builtins напрямую

В модуле builtins доступны isinstance, callable и тип complex. Импортируйте модуль, достаньте нужные атрибуты и вызовите их для объекта, который вы проверяете.

bool isComplexObj(py::object obj)
{
    py::object py_builtins = py::module_::import("builtins");
    py::object fn_isinstance = py_builtins.attr("isinstance");
    py::object cls_complex = py_builtins.attr("complex");
    // Пусть ошибки типов всплывают, если случится что-то неожиданное
    return py::cast<py::bool_>(fn_isinstance(obj, cls_complex));
}
bool isCallableObj(py::object obj)
{
    py::object py_builtins = py::module_::import("builtins");
    py::object fn_callable = py_builtins.attr("callable");
    return py::cast<py::bool_>(fn_callable(obj));
}

В Python это работает именно так, как вы ожидаете:

import pybind_sample
z = 5 + 4j
print(pybind_sample.isComplexObj(z))  # выведет True

Вы можете сохранить эти объекты где‑нибудь, чтобы не импортировать их каждый раз, если только не планируете использовать несколько интерпретаторов или перезапускать интерпретатор.

Почему это важно

Отказ от eval снимает накладные расходы и уменьшает потенциальную поверхность атаки ваших биндингов. Код становится понятнее, его проще отлаживать, он меньше ломается. Главное — вы опираетесь на канонические механизмы Python для проверки типов и вызываемости, а не на строковые конструкции и общие буферы.

Итоги

Если в pybind11 нужно распознать комплексные числа или функциональные объекты, не собирайте строки для eval. Импортируйте builtins, возьмите isinstance, complex и callable и вызывайте их напрямую. Так биндинги остаются чистыми и предсказуемыми; при повторных вызовах эти объекты можно дополнительно кешировать — с оговоркой про множественные интерпретаторы или перезапуски интерпретатора.