Saltar a contenido

Última actualización:2026-10-12

Notas sobre el desarrollo de extensiones de CPython

Este artículo girará en torno al trasfondo de una feature que envié recientemente a Apache para hablar de los problemas encontrados con CPython y del intento de escribir una biblioteca propia desde cero; el código también está en la comunidad 星球 y quien esté interesado puede descargarlo para estudiarlo.

0. Contexto

Últimamente, una feature que envié a apache arrow provocó, por un cambio en las interfaces de C++, cambios en las interfaces de otros lenguajes, y algunas pruebas hubo que repararlas en consecuencia.

Quien conozca PyArrow no le resultará nada nuevo: este cambio de interfaz también obligó a modificar esa biblioteca, ya que está en el mismo repositorio; de lo contrario, el ci no pasa. Y la implementación de PyArrow se hace mediante CPython; como yo no había estudiado CPython en especial, introduje un montón de problemas. Dos de ellos son bastante importantes y los registro aquí.

Problema 1: la función de inicialización no admite la construcción por defecto de otras clases.

Ejemplo:

 def __init__(self, mode="only_valid", filter=Expression._scalar(True)):
        pass

Error:

TypeError: descriptor '_scalar' for 'pyarrow._compute.Expression' objects doesn't apply to a 'bool' object

Se ve que no lo reconoce, aunque en realidad Expression._scalar(True) es válido; miremos su implementación:

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

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

    return Expression.wrap(CMakeScalarExpression(scalar.unwrap()))

Como se ve, dentro admite el tipo bool normal; sospeché que era una limitación de cpython, así que cambié a la forma siguiente y ya funcionó:

def __init__(self, mode="only_valid", filter=None):
    if filter is None:
        filter = Expression._scalar(True)

Problema 2: el orden de definición

Al usar el _true creado más abajo, cada vez que se pasa como valor por defecto llega vacío; esto se entiende bien, porque al final, una vez compilado, todo se traduce a un archivo xxx.cpp y, según las reglas de C++, lo que se lee antes está naturalmente vacío.

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


cdef CExpression _true = CMakeScalarExpression(
    <shared_ptr[CScalar]> make_shared[CBooleanScalar](True)
)

Bien, con este trasfondo, a mí también me apetecía escribir un ejemplo: usar C++ para escribir una clase que encapsule sort y sum, y luego llamarla desde Python.

1. Ejemplo completo de CPython

  1. Crear un archivo .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) Crear foo.pyx

Punto importante: el vector anterior necesita:

from libcpp.vector cimport vector

Después define una class que llame a la interfaz de 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)
  1. Crear el archivo setup.py
ext = Extension('Foo', sources=["foo.pyx"], language="c++", include_dirs=[numpy.get_include()])

setup(name="Foo", ext_modules = cythonize([ext]))
  1. Ejecutar
python3 setup.py build_ext --inplace

Por último, puedes escribir un script de prueba para usar tu propia interfaz de 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 se usa bastante en algunos proyectos; ¡ponte a aprenderlo~

Ejecución:

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

Temas relacionados

Comentarios