Перейти к содержанию

Последнее обновление:2026-10-12

Заметки о разработке расширений CPython

Эта статья на фоне недавно отправленного в Apache feature расскажет о проблемах, возникших с CPython, и о попытке написать собственную библиотеку с нуля; код уже выложен в закрытом сообществе — все желающие могут скачать и изучить его.

0. Предыстория

Недавно отправленный мной в apache arrow feature из-за изменения C++-интерфейса повлёк изменение интерфейсов других языков, и некоторые тесты тоже пришлось чинить.

Те, кто знаком с PyArrow, прекрасно это поймут: на этот раз при изменении интерфейса пришлось править и эту библиотеку, так как она находится в том же репозитории — иначе CI не проходил. А реализация PyArrow сделана через CPython; раньше я толком не изучал CPython и в процессе правок натворил кучу проблем. Две из них оказались особенно важными — запишу их здесь.

Проблема 1: внутри инициализирующей функции не поддерживается вызов конструктора по умолчанию других классов.

Пример:

 def __init__(self, mode="only_valid", filter=Expression._scalar(True)):
        pass
Ошибка:

TypeError: descriptor '_scalar' for 'pyarrow._compute.Expression' objects doesn't apply to a 'bool' object
Видно, что распознать не удалось, хотя на самом деле Expression._scalar(True) — корректный вызов. Посмотрим на внутреннюю реализацию:

@staticmethod
def _scalar(value):
    cdef:
        Scalar scalar

    if isinstance(value, Scalar):
        scalar = value
    else:
        scalar = lib.scalar(value)

    return Expression.wrap(CMakeScalarExpression(scalar.unwrap()))
Как видно, внутри поддерживается обычный тип bool. Я заподозрил, что это ограничение cpython, и после замены на следующий вариант всё заработало:

def __init__(self, mode="only_valid", filter=None):
    if filter is None:
        filter = Expression._scalar(True)
Проблема 2: порядок определения

Когда я использовал созданный позже _true, каждый раз передаваемое значение по умолчанию оказывалось пустым. Это довольно понятно: в итоге всё компилируется в один файл xxx.cpp, и по правилам C++ при чтении раньше определения там, естественно, пусто.

def __init__(self, mode="only_valid", filter=_true):
        pass


cdef CExpression _true = CMakeScalarExpression(
    <shared_ptr[CScalar]> make_shared[CBooleanScalar](True)
)
Итак, на основе всего этого мне захотелось написать собственный пример: создать класс на C++, инкапсулировать sort и sum, а затем вызывать его из Python.

1. Полный пример на CPython

  1. Создаём файл .h

void sort(std::vector<int>& nums) {
    std::sort(nums.begin(), nums.end());
}
int sum(std::vector<int>& nums) {
    int sum = 0;
    for (int num : nums) {
        sum += num;
    }
    return sum;
}
2) Создаём foo.pyx

Важный момент: для приведённого выше vector нужно:

from libcpp.vector cimport vector
Затем определяем class и вызываем C++-интерфейс.

cdef class PyFoo:
    cdef Foo* f

    def __cinit__(self):
        self.f = new Foo()

    def __dealloc__(self):
        del self.f

    def sort(self, nums):
        cdef vector[int] c_nums = nums
        self.f.sort(c_nums)

    def sum(self, nums):
        cdef vector[int] c_nums = nums
        return self.f.sum(c_nums)
3. Создаём файл setup.py

ext = Extension('Foo', sources=["foo.pyx"], language="c++", include_dirs=[numpy.get_include()])

setup(name="Foo", ext_modules = cythonize([ext]))
4. Запуск

python3 setup.py build_ext --inplace
Наконец, можно написать тестовый сценарий и использовать собственный python-интерфейс.

import Foo

f = Foo.PyFoo()
nums = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
f.sort(nums)
print("Sorted nums:", nums)
print("Sum of nums:", f.sum(nums))
CPython используется во многих проектах — учим его~

Запуск:

➜  cpython_examples python3 test.py 
Sorted nums: [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
Sum of nums: 44

Похожие темы

Комментарии