2. Definición de tipos de extensión: Tutorial

Python le permite al escritor de un módulo de extensión C definir nuevos tipos que pueden ser manipulados desde el código Python, al igual que los tipos incorporados str y list. El código para todos los tipos de extensión sigue un patrón, pero hay algunos detalles que debe comprender antes de comenzar. Este documento es una introducción suave al tema.

2.1. Lo Básico

El tiempo de ejecución CPython ve todos los objetos de Python como variables de tipo PyObject*, que sirve como un «tipo base» para todos los objetos de Python. La estructura PyObject solo contiene el reference count del objeto y un puntero al «objeto tipo» del objeto. Aquí es donde está la acción; el objeto tipo determina qué funciones (C) llama el intérprete cuando, por ejemplo, se busca un atributo en un objeto, se llama un método o se multiplica por otro objeto. Estas funciones de C se denominan «métodos de tipo».

Por lo tanto, si desea definir un nuevo tipo de extensión, debe crear un nuevo objeto de tipo.

Este tipo de cosas solo se pueden explicar con un ejemplo, por lo que aquí hay un módulo mínimo, pero completo, que define un nuevo tipo llamado Custom dentro de un módulo de extensión C custom:

Nota

Lo que estamos mostrando aquí es la forma tradicional de definir tipos de extensión estáticos. Debe ser adecuado para la mayoría de los usos. La API de C también permite definir tipos de extensiones asignadas en el montón utilizando la función PyType_FromSpec(), que no se trata en este tutorial.

#define PY_SSIZE_T_CLEAN
#include <Python.h>

typedef struct {
    PyObject_HEAD
    /* Type-specific fields go here. */
} CustomObject;

static PyTypeObject CustomType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "custom.Custom",
    .tp_doc = "Custom objects",
    .tp_basicsize = sizeof(CustomObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT,
    .tp_new = PyType_GenericNew,
};

static PyModuleDef custommodule = {
    PyModuleDef_HEAD_INIT,
    .m_name = "custom",
    .m_doc = "Example module that creates an extension type.",
    .m_size = -1,
};

PyMODINIT_FUNC
PyInit_custom(void)
{
    PyObject *m;
    if (PyType_Ready(&CustomType) < 0)
        return NULL;

    m = PyModule_Create(&custommodule);
    if (m == NULL)
        return NULL;

    Py_INCREF(&CustomType);
    if (PyModule_AddObject(m, "Custom", (PyObject *) &CustomType) < 0) {
        Py_DECREF(&CustomType);
        Py_DECREF(m);
        return NULL;
    }

    return m;
}

Ahora, eso es bastante para asimilar a la vez, pero espero que los fragmentos le resulten familiares del capítulo anterior. Este archivo define tres cosas:

  1. Lo que contiene un objeto Custom: esta es la estructura CustomObject, que se asigna una vez para cada instancia de Custom.

  2. Cómo se comporta Custom type: esta es la estructura CustomType, que define un conjunto de indicadores y punteros de función que el intérprete inspecciona cuando se solicitan operaciones específicas.

  3. Cómo inicializar el módulo custom: esta es la función PyInit_custom y la estructura asociada custommodule.

La primera parte es:

typedef struct {
    PyObject_HEAD
} CustomObject;

Esto es lo que contendrá un objeto personalizado. PyObject_HEAD es obligatorio al comienzo de cada estructura de objeto y define un campo llamado ob_base de tipo PyObject, que contiene un puntero a un objeto de tipo y un recuento de referencia (estos pueden ser accedidos mediante las macros Py_REFCNT y Py_TYPE respectivamente). El motivo de la macro es abstraer el diseño y habilitar campos adicionales en las compilaciones de depuración.

Nota

No hay punto y coma (;) arriba después de la macro PyObject_HEAD. Tenga cuidado de agregar uno por accidente: algunos compiladores se quejarán.

Por supuesto, los objetos generalmente almacenan datos adicionales además del estándar PyObject_HEAD repetitivo; por ejemplo, aquí está la definición de puntos flotantes del estándar de Python:

typedef struct {
    PyObject_HEAD
    double ob_fval;
} PyFloatObject;

La segunda parte es la definición del tipo de objecto.

static PyTypeObject CustomType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "custom.Custom",
    .tp_doc = "Custom objects",
    .tp_basicsize = sizeof(CustomObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT,
    .tp_new = PyType_GenericNew,
};

Nota

Recomendamos utilizar los inicializadores designados al estilo C99 como se indica arriba, para evitar enumerar todos los campos PyTypeObject que no le interesan y también para evitar preocuparse por el orden de declaración de los campos.

La definición real de PyTypeObject en object.h tiene muchos más campos que la definición anterior. El compilador de C rellenará los campos restantes con ceros, y es una práctica común no especificarlos explícitamente a menos que los necesite.

Lo vamos a separar, un campo a la vez:

PyVarObject_HEAD_INIT(NULL, 0)

Esta línea es obligatoria para inicializar el campo ob_base mencionado anteriormente.

.tp_name = "custom.Custom",

El nombre de nuestro tipo. Esto aparecerá en la representación textual predeterminada de nuestros objetos y en algunos mensajes de error, por ejemplo:

>>> "" + custom.Custom()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can only concatenate str (not "custom.Custom") to str

Tenga en cuenta que el nombre es un nombre punteado que incluye tanto el nombre del módulo como el nombre del tipo dentro del módulo. El módulo en este caso es custom y el tipo es Custom, por lo que establecemos el nombre del tipo en custom.Custom. Usar la ruta de importación punteada real es importante para que su tipo sea compatible con los módulos pydoc y pickle.

.tp_basicsize = sizeof(CustomObject),
.tp_itemsize = 0,

Esto es para que Python sepa cuánta memoria asignar al crear instancias nuevas Custom. tp_itemsize solo se usa para objetos de tamaño variable y, de lo contrario, debería ser cero.

Nota

Si desea que su tipo pueda tener subclases desde Python, y su tipo tiene el mismo tp_basicsize como su tipo base, puede tener problemas con la herencia múltiple. Una subclase de Python de su tipo tendrá que enumerar su tipo primero en su __bases__, o de lo contrario no podrá llamar al método de su tipo __new__() sin obtener un error. Puede evitar este problema asegurándose de que su tipo tenga un valor mayor para tp_basicsize que su tipo base. La mayoría de las veces, esto será cierto de todos modos, porque su tipo base será object, o de lo contrario agregará miembros de datos a su tipo base y, por lo tanto, aumentará su tamaño.

Configuramos las banderas de clase a Py_TPFLAGS_DEFAULT.

.tp_flags = Py_TPFLAGS_DEFAULT,

Todos los tipos deben incluir esta constante en sus banderas. Habilita todos los miembros definidos hasta al menos Python 3.3. Si necesita más miembros, necesitará O (OR) las banderas correspondientes.

Proporcionamos una cadena de documentos para el tipo en tp_doc.

.tp_doc = "Custom objects",

Para habilitar la creación de objetos, debemos proporcionar un controlador tp_new. Este es el equivalente del método Python __new__(), pero debe especificarse explícitamente. En este caso, podemos usar la implementación predeterminada proporcionada por la función API PyType_GenericNew().

.tp_new = PyType_GenericNew,

Todo lo demás en el archivo debe ser familiar, excepto algún código en PyInit_custom():

if (PyType_Ready(&CustomType) < 0)
    return;

Esto inicializa el tipo Custom, completando un número de miembros con los valores predeterminados apropiados, que incluyen ob_type que inicialmente configuramos en NULL.

Py_INCREF(&CustomType);
if (PyModule_AddObject(m, "Custom", (PyObject *) &CustomType) < 0) {
    Py_DECREF(&CustomType);
    Py_DECREF(m);
    return NULL;
}

Esto agrega el tipo al diccionario del módulo. Esto nos permite crear instancias Custom llamando la clase Custom:

>>> import custom
>>> mycustom = custom.Custom()

¡Eso es! Todo lo que queda es construirlo; ponga el código anterior en un archivo llamado custom.c y:

from distutils.core import setup, Extension
setup(name="custom", version="1.0",
      ext_modules=[Extension("custom", ["custom.c"])])

en un archivo llamado setup.py; luego escribiendo

$ python setup.py build

en un shell debería producir un archivo custom.so en un subdirectorio; muévete a ese directorio y abre Python — deberías poder import custom y jugar con objetos personalizados.

Eso no fue tan difícil, ¿verdad?

Por supuesto, el tipo personalizado actual es bastante poco interesante. No tiene datos y no hace nada. Ni siquiera se puede subclasificar.

Nota

Si bien esta documentación muestra el módulo estándar distutils para construir extensiones C, se recomienda en casos de uso del mundo real utilizar la biblioteca setuptools más nueva y mejor mantenida. La documentación sobre cómo hacer esto está fuera del alcance de este documento y se puede encontrar en la Guía de usuario del Empaquetamiento de Python.

2.2. Agregar datos y métodos al ejemplo básico

Extendamos el ejemplo básico para agregar algunos datos y métodos. También hagamos que el tipo sea utilizable como una clase base. Crearemos un nuevo módulo, custom2 que agrega estas capacidades:

#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include "structmember.h"

typedef struct {
    PyObject_HEAD
    PyObject *first; /* first name */
    PyObject *last;  /* last name */
    int number;
} CustomObject;

static void
Custom_dealloc(CustomObject *self)
{
    Py_XDECREF(self->first);
    Py_XDECREF(self->last);
    Py_TYPE(self)->tp_free((PyObject *) self);
}

static PyObject *
Custom_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    CustomObject *self;
    self = (CustomObject *) type->tp_alloc(type, 0);
    if (self != NULL) {
        self->first = PyUnicode_FromString("");
        if (self->first == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->last = PyUnicode_FromString("");
        if (self->last == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->number = 0;
    }
    return (PyObject *) self;
}

static int
Custom_init(CustomObject *self, PyObject *args, PyObject *kwds)
{
    static char *kwlist[] = {"first", "last", "number", NULL};
    PyObject *first = NULL, *last = NULL, *tmp;

    if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOi", kwlist,
                                     &first, &last,
                                     &self->number))
        return -1;

    if (first) {
        tmp = self->first;
        Py_INCREF(first);
        self->first = first;
        Py_XDECREF(tmp);
    }
    if (last) {
        tmp = self->last;
        Py_INCREF(last);
        self->last = last;
        Py_XDECREF(tmp);
    }
    return 0;
}

static PyMemberDef Custom_members[] = {
    {"first", T_OBJECT_EX, offsetof(CustomObject, first), 0,
     "first name"},
    {"last", T_OBJECT_EX, offsetof(CustomObject, last), 0,
     "last name"},
    {"number", T_INT, offsetof(CustomObject, number), 0,
     "custom number"},
    {NULL}  /* Sentinel */
};

static PyObject *
Custom_name(CustomObject *self, PyObject *Py_UNUSED(ignored))
{
    if (self->first == NULL) {
        PyErr_SetString(PyExc_AttributeError, "first");
        return NULL;
    }
    if (self->last == NULL) {
        PyErr_SetString(PyExc_AttributeError, "last");
        return NULL;
    }
    return PyUnicode_FromFormat("%S %S", self->first, self->last);
}

static PyMethodDef Custom_methods[] = {
    {"name", (PyCFunction) Custom_name, METH_NOARGS,
     "Return the name, combining the first and last name"
    },
    {NULL}  /* Sentinel */
};

static PyTypeObject CustomType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "custom2.Custom",
    .tp_doc = "Custom objects",
    .tp_basicsize = sizeof(CustomObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
    .tp_new = Custom_new,
    .tp_init = (initproc) Custom_init,
    .tp_dealloc = (destructor) Custom_dealloc,
    .tp_members = Custom_members,
    .tp_methods = Custom_methods,
};

static PyModuleDef custommodule = {
    PyModuleDef_HEAD_INIT,
    .m_name = "custom2",
    .m_doc = "Example module that creates an extension type.",
    .m_size = -1,
};

PyMODINIT_FUNC
PyInit_custom2(void)
{
    PyObject *m;
    if (PyType_Ready(&CustomType) < 0)
        return NULL;

    m = PyModule_Create(&custommodule);
    if (m == NULL)
        return NULL;

    Py_INCREF(&CustomType);
    if (PyModule_AddObject(m, "Custom", (PyObject *) &CustomType) < 0) {
        Py_DECREF(&CustomType);
        Py_DECREF(m);
        return NULL;
    }

    return m;
}

Esta versión del módulo tiene una serie de cambios.

Hemos agregado una inclusión adicional:

#include <structmember.h>

Esto incluye declaraciones que usamos para manejar atributos, como se describe un poco más adelante.

El tipo Custom ahora tiene tres atributos de datos en su estructura C, first, last y number. Las variables first y last son cadenas de caracteres de Python que contienen nombres y apellidos. El atributo number es un entero C.

La estructura del objeto se actualiza en consecuencia:

typedef struct {
    PyObject_HEAD
    PyObject *first; /* first name */
    PyObject *last;  /* last name */
    int number;
} CustomObject;

Debido a que ahora tenemos datos para administrar, debemos ser más cuidadosos con la asignación de objetos y la desasignación. Como mínimo, necesitamos un método de desasignación:

static void
Custom_dealloc(CustomObject *self)
{
    Py_XDECREF(self->first);
    Py_XDECREF(self->last);
    Py_TYPE(self)->tp_free((PyObject *) self);
}

que se asigna al miembro tp_dealloc:

.tp_dealloc = (destructor) Custom_dealloc,

Este método primero borra los recuentos de referencia de los dos atributos de Python. Py_XDECREF() maneja correctamente el caso donde su argumento es NULL (lo que podría ocurrir aquí si tp_new fallara a mitad de camino). Luego llama al miembro tp_free del tipo de objeto (calculado por Py_TYPE(self)) para liberar la memoria del objeto. Tenga en cuenta que el tipo de objeto podría no ser CustomType, porque el objeto puede ser una instancia de una subclase.

Nota

La conversión explícita a destructor anterior es necesaria porque definimos Custom_dealloc para tomar un argumento CustomObject *, pero el puntero de función tp_dealloc espera recibir un argumento PyObject *. De lo contrario, el compilador emitirá una advertencia. Este es un polimorfismo orientado a objetos, en C!

Queremos asegurarnos de que el nombre y el apellido se inicialicen en cadenas de caracteres vacías, por lo que proporcionamos una implementación tp_new:

static PyObject *
Custom_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    CustomObject *self;
    self = (CustomObject *) type->tp_alloc(type, 0);
    if (self != NULL) {
        self->first = PyUnicode_FromString("");
        if (self->first == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->last = PyUnicode_FromString("");
        if (self->last == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->number = 0;
    }
    return (PyObject *) self;
}

e instalarlo en el miembro tp_new:

.tp_new = Custom_new,

El controlador tp_new es responsable de crear (en lugar de inicializar) objetos del tipo. Está expuesto en Python como el método __new__(). No es necesario definir un miembro tp_new, y de hecho muchos tipos de extensiones simplemente reutilizarán PyType_GenericNew() como se hizo en la primera versión del tipo Personalizado anterior. En este caso, usamos el controlador tp_new para inicializar los atributos first y last a valores predeterminados que no sean NULL.

tp_new se pasa el tipo que se instancia (no necesariamente CustomType, si se instancia una subclase) y cualquier argumento pasado cuando se llamó al tipo, y se espera que retorna la instancia creada. Los manejadores tp_new siempre aceptan argumentos posicionales y de palabras clave, pero a menudo ignoran los argumentos, dejando el manejo de argumentos al inicializador (también conocido como, tp_init en C o __init__ en Python).

Nota

tp_new no debería llamar explícitamente a tp_init, ya que el intérprete lo hará por sí mismo.

La implementación tp_new llama al tp_alloc para asignar memoria:

self = (CustomObject *) type->tp_alloc(type, 0);

Como la asignación de memoria puede fallar, debemos verificar el resultado tp_alloc contra NULL antes de continuar.

Nota

No llenamos la ranura tp_alloc nosotros mismos. Más bien PyType_Ready() lo llena para nosotros al heredarlo de nuestra clase base, que es object por defecto. La mayoría de los tipos utilizan la estrategia de asignación predeterminada.

Nota

Si está creando una cooperativa tp_new (una que llama a un tipo base tp_new o __new__()), no debe intentar determinar a qué método llamar utilizando el orden de resolución del método en tiempo de ejecución. Siempre determine estáticamente a qué tipo va a llamar, y llame a su tp_new directamente, o mediante type->tp_base->tp_new. Si no hace esto, las subclases de Python de su tipo que también heredan de otras clases definidas por Python pueden no funcionar correctamente. (Específicamente, es posible que no pueda crear instancias de tales subclases sin obtener un TypeError).

También definimos una función de inicialización que acepta argumentos para proporcionar valores iniciales para nuestra instancia:

static int
Custom_init(CustomObject *self, PyObject *args, PyObject *kwds)
{
    static char *kwlist[] = {"first", "last", "number", NULL};
    PyObject *first = NULL, *last = NULL, *tmp;

    if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOi", kwlist,
                                     &first, &last,
                                     &self->number))
        return -1;

    if (first) {
        tmp = self->first;
        Py_INCREF(first);
        self->first = first;
        Py_XDECREF(tmp);
    }
    if (last) {
        tmp = self->last;
        Py_INCREF(last);
        self->last = last;
        Py_XDECREF(tmp);
    }
    return 0;
}

rellenando la ranura tp_init.

.tp_init = (initproc) Custom_init,

La ranura tp_init está expuesta en Python como el método __init__(). Se utiliza para inicializar un objeto una vez creado. Los inicializadores siempre aceptan argumentos posicionales y de palabras clave, y deben retornar 0 en caso de éxito o -1 en caso de error.

A diferencia del controlador tp_new, no hay garantía de que se llame a tp_init (por ejemplo, el módulo pickle por defecto no llama a __init__() en instancias no controladas ) También se puede llamar varias veces. Cualquiera puede llamar al método __init__() en nuestros objetos. Por esta razón, debemos tener mucho cuidado al asignar los nuevos valores de atributo. Podríamos sentirnos tentados, por ejemplo, a asignar el primer miembro de esta manera:

if (first) {
    Py_XDECREF(self->first);
    Py_INCREF(first);
    self->first = first;
}

But this would be risky. Our type doesn’t restrict the type of the first member, so it could be any kind of object. It could have a destructor that causes code to be executed that tries to access the first member; or that destructor could release the Global interpreter Lock and let arbitrary code run in other threads that accesses and modifies our object.

Para ser paranoicos y protegernos de esta posibilidad, casi siempre reasignamos miembros antes de disminuir sus recuentos de referencias. ¿Cuándo no tenemos que hacer esto?

  • cuando sabemos absolutamente que el recuento de referencia es mayor que 1;

  • cuando sabemos que la desasignación del objeto 1 no liberará el GIL ni causará ninguna llamada al código de nuestro tipo;

  • al disminuir un recuento de referencias en un manejador tp_dealloc en un tipo que no admite la recolección de basura cíclica 2.

Queremos exponer nuestras variables de instancia como atributos. Hay varias formas de hacerlo. La forma más simple es definir definiciones de miembros:

static PyMemberDef Custom_members[] = {
    {"first", T_OBJECT_EX, offsetof(CustomObject, first), 0,
     "first name"},
    {"last", T_OBJECT_EX, offsetof(CustomObject, last), 0,
     "last name"},
    {"number", T_INT, offsetof(CustomObject, number), 0,
     "custom number"},
    {NULL}  /* Sentinel */
};

y poner las definiciones en la ranura tp_members:

.tp_members = Custom_members,

Cada definición de miembro tiene un nombre de miembro, tipo, desplazamiento, banderas de acceso y cadena de caracteres de documentación. Consulte la sección Gestión de atributos genéricos a continuación para obtener más detalles.

Una desventaja de este enfoque es que no proporciona una forma de restringir los tipos de objetos que se pueden asignar a los atributos de Python. Esperamos que el nombre y el apellido sean cadenas, pero se pueden asignar objetos de Python. Además, los atributos se pueden eliminar, configurando los punteros C en NULL. Si bien podemos asegurarnos de que los miembros se inicialicen en valores que no sean NULL, los miembros se pueden establecer en NULL si se eliminan los atributos.

Definimos un método único, Custom.name(), que genera el nombre de los objetos como la concatenación de los nombres y apellidos.

static PyObject *
Custom_name(CustomObject *self, PyObject *Py_UNUSED(ignored))
{
    if (self->first == NULL) {
        PyErr_SetString(PyExc_AttributeError, "first");
        return NULL;
    }
    if (self->last == NULL) {
        PyErr_SetString(PyExc_AttributeError, "last");
        return NULL;
    }
    return PyUnicode_FromFormat("%S %S", self->first, self->last);
}

El método se implementa como una función C que toma una instancia de Custom (o subclase Custom) como primer argumento. Los métodos siempre toman una instancia como primer argumento. Los métodos a menudo también toman argumentos posicionales y de palabras clave, pero en este caso no tomamos ninguno y no necesitamos aceptar una tupla de argumentos posicionales o un diccionario de argumentos de palabras clave. Este método es equivalente al método Python:

def name(self):
    return "%s %s" % (self.first, self.last)

Tenga en cuenta que tenemos que verificar la posibilidad de que nuestros miembros first y last sean NULL. Esto se debe a que se pueden eliminar, en cuyo caso se establecen en NULL. Sería mejor evitar la eliminación de estos atributos y restringir los valores de los atributos para que sean cadenas de caracteres. Veremos cómo hacerlo en la siguiente sección.

Ahora que hemos definido el método, necesitamos crear un arreglo de definiciones de métodos:

static PyMethodDef Custom_methods[] = {
    {"name", (PyCFunction) Custom_name, METH_NOARGS,
     "Return the name, combining the first and last name"
    },
    {NULL}  /* Sentinel */
};

(tenga en cuenta que usamos el indicador METH_NOARGS para indicar que el método no espera argumentos distintos de self)

y asignarlo a la ranura tp_methods:

.tp_methods = Custom_methods,

Finalmente, haremos que nuestro tipo sea utilizable como una clase base para la subclase. Hemos escrito nuestros métodos cuidadosamente hasta ahora para que no hagan suposiciones sobre el tipo de objeto que se está creando o utilizando, por lo que todo lo que tenemos que hacer es agregar Py_TPFLAGS_BASETYPE a nuestra definición de bandera de clase:

.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,

Cambiamos el nombre de PyInit_custom() a PyInit_custom2(), actualizamos el nombre del módulo en la estructura PyModuleDef y actualizamos el nombre completo de la clase en la estructura PyTypeObject.

Finalmente, actualizamos nuestro archivo setup.py para construir el nuevo módulo:

from distutils.core import setup, Extension
setup(name="custom", version="1.0",
      ext_modules=[
         Extension("custom", ["custom.c"]),
         Extension("custom2", ["custom2.c"]),
         ])

2.3. Proporcionar un control más preciso sobre los atributos de datos

En esta sección, proporcionaremos un control más preciso sobre cómo se establecen los atributos first y last en el ejemplo Custom. En la versión anterior de nuestro módulo, las variables de instancia first y last podrían establecerse en valores que no sean de cadena o incluso eliminarse. Queremos asegurarnos de que estos atributos siempre contengan cadenas.

#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include "structmember.h"

typedef struct {
    PyObject_HEAD
    PyObject *first; /* first name */
    PyObject *last;  /* last name */
    int number;
} CustomObject;

static void
Custom_dealloc(CustomObject *self)
{
    Py_XDECREF(self->first);
    Py_XDECREF(self->last);
    Py_TYPE(self)->tp_free((PyObject *) self);
}

static PyObject *
Custom_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    CustomObject *self;
    self = (CustomObject *) type->tp_alloc(type, 0);
    if (self != NULL) {
        self->first = PyUnicode_FromString("");
        if (self->first == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->last = PyUnicode_FromString("");
        if (self->last == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->number = 0;
    }
    return (PyObject *) self;
}

static int
Custom_init(CustomObject *self, PyObject *args, PyObject *kwds)
{
    static char *kwlist[] = {"first", "last", "number", NULL};
    PyObject *first = NULL, *last = NULL, *tmp;

    if (!PyArg_ParseTupleAndKeywords(args, kwds, "|UUi", kwlist,
                                     &first, &last,
                                     &self->number))
        return -1;

    if (first) {
        tmp = self->first;
        Py_INCREF(first);
        self->first = first;
        Py_DECREF(tmp);
    }
    if (last) {
        tmp = self->last;
        Py_INCREF(last);
        self->last = last;
        Py_DECREF(tmp);
    }
    return 0;
}

static PyMemberDef Custom_members[] = {
    {"number", T_INT, offsetof(CustomObject, number), 0,
     "custom number"},
    {NULL}  /* Sentinel */
};

static PyObject *
Custom_getfirst(CustomObject *self, void *closure)
{
    Py_INCREF(self->first);
    return self->first;
}

static int
Custom_setfirst(CustomObject *self, PyObject *value, void *closure)
{
    PyObject *tmp;
    if (value == NULL) {
        PyErr_SetString(PyExc_TypeError, "Cannot delete the first attribute");
        return -1;
    }
    if (!PyUnicode_Check(value)) {
        PyErr_SetString(PyExc_TypeError,
                        "The first attribute value must be a string");
        return -1;
    }
    tmp = self->first;
    Py_INCREF(value);
    self->first = value;
    Py_DECREF(tmp);
    return 0;
}

static PyObject *
Custom_getlast(CustomObject *self, void *closure)
{
    Py_INCREF(self->last);
    return self->last;
}

static int
Custom_setlast(CustomObject *self, PyObject *value, void *closure)
{
    PyObject *tmp;
    if (value == NULL) {
        PyErr_SetString(PyExc_TypeError, "Cannot delete the last attribute");
        return -1;
    }
    if (!PyUnicode_Check(value)) {
        PyErr_SetString(PyExc_TypeError,
                        "The last attribute value must be a string");
        return -1;
    }
    tmp = self->last;
    Py_INCREF(value);
    self->last = value;
    Py_DECREF(tmp);
    return 0;
}

static PyGetSetDef Custom_getsetters[] = {
    {"first", (getter) Custom_getfirst, (setter) Custom_setfirst,
     "first name", NULL},
    {"last", (getter) Custom_getlast, (setter) Custom_setlast,
     "last name", NULL},
    {NULL}  /* Sentinel */
};

static PyObject *
Custom_name(CustomObject *self, PyObject *Py_UNUSED(ignored))
{
    return PyUnicode_FromFormat("%S %S", self->first, self->last);
}

static PyMethodDef Custom_methods[] = {
    {"name", (PyCFunction) Custom_name, METH_NOARGS,
     "Return the name, combining the first and last name"
    },
    {NULL}  /* Sentinel */
};

static PyTypeObject CustomType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "custom3.Custom",
    .tp_doc = "Custom objects",
    .tp_basicsize = sizeof(CustomObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
    .tp_new = Custom_new,
    .tp_init = (initproc) Custom_init,
    .tp_dealloc = (destructor) Custom_dealloc,
    .tp_members = Custom_members,
    .tp_methods = Custom_methods,
    .tp_getset = Custom_getsetters,
};

static PyModuleDef custommodule = {
    PyModuleDef_HEAD_INIT,
    .m_name = "custom3",
    .m_doc = "Example module that creates an extension type.",
    .m_size = -1,
};

PyMODINIT_FUNC
PyInit_custom3(void)
{
    PyObject *m;
    if (PyType_Ready(&CustomType) < 0)
        return NULL;

    m = PyModule_Create(&custommodule);
    if (m == NULL)
        return NULL;

    Py_INCREF(&CustomType);
    if (PyModule_AddObject(m, "Custom", (PyObject *) &CustomType) < 0) {
        Py_DECREF(&CustomType);
        Py_DECREF(m);
        return NULL;
    }

    return m;
}

Para proporcionar un mayor control sobre los atributos first y last, usaremos funciones personalizadas getter y setter. Estas son las funciones para obtener y configurar el atributo first:

static PyObject *
Custom_getfirst(CustomObject *self, void *closure)
{
    Py_INCREF(self->first);
    return self->first;
}

static int
Custom_setfirst(CustomObject *self, PyObject *value, void *closure)
{
    PyObject *tmp;
    if (value == NULL) {
        PyErr_SetString(PyExc_TypeError, "Cannot delete the first attribute");
        return -1;
    }
    if (!PyUnicode_Check(value)) {
        PyErr_SetString(PyExc_TypeError,
                        "The first attribute value must be a string");
        return -1;
    }
    tmp = self->first;
    Py_INCREF(value);
    self->first = value;
    Py_DECREF(tmp);
    return 0;
}

La función getter se pasa al objeto Custom y un «cierre» (closure), que es un puntero nulo. En este caso, se ignora el cierre. (El cierre admite un uso avanzado en el que los datos de definición se pasan al captador y al definidor. Esto podría, por ejemplo, usarse para permitir un solo conjunto de funciones de captador y definidor que deciden que el atributo se obtenga o establezca en función de los datos en el cierre.)

La función setter pasa el objeto Custom, el nuevo valor y el cierre. El nuevo valor puede ser NULL, en cuyo caso se está eliminando el atributo. En nuestro setter, generamos un error si el atributo se elimina o si su nuevo valor no es una cadena.

Creamos un arreglo de estructuras PyGetSetDef:

static PyGetSetDef Custom_getsetters[] = {
    {"first", (getter) Custom_getfirst, (setter) Custom_setfirst,
     "first name", NULL},
    {"last", (getter) Custom_getlast, (setter) Custom_setlast,
     "last name", NULL},
    {NULL}  /* Sentinel */
};

y lo registra en la ranura tp_getset:

.tp_getset = Custom_getsetters,

El último elemento en la estructura PyGetSetDef es el «cierre» (closure) mencionado anteriormente. En este caso, no estamos usando un cierre, por lo que simplemente pasamos NULL.

También eliminamos las definiciones de miembro para estos atributos:

static PyMemberDef Custom_members[] = {
    {"number", T_INT, offsetof(CustomObject, number), 0,
     "custom number"},
    {NULL}  /* Sentinel */
};

También necesitamos actualizar el manejador tp_init para permitir que solo se pasen las cadenas 3:

static int
Custom_init(CustomObject *self, PyObject *args, PyObject *kwds)
{
    static char *kwlist[] = {"first", "last", "number", NULL};
    PyObject *first = NULL, *last = NULL, *tmp;

    if (!PyArg_ParseTupleAndKeywords(args, kwds, "|UUi", kwlist,
                                     &first, &last,
                                     &self->number))
        return -1;

    if (first) {
        tmp = self->first;
        Py_INCREF(first);
        self->first = first;
        Py_DECREF(tmp);
    }
    if (last) {
        tmp = self->last;
        Py_INCREF(last);
        self->last = last;
        Py_DECREF(tmp);
    }
    return 0;
}

Con estos cambios, podemos asegurar que los miembros primero y último nunca sean NULL, por lo que podemos eliminar las comprobaciones de los valores NULL en casi todos los casos. Esto significa que la mayoría de las llamadas Py_XDECREF() se pueden convertir en llamadas Py_DECREF(). El único lugar donde no podemos cambiar estas llamadas es en la implementación tp_dealloc, donde existe la posibilidad de que la inicialización de estos miembros falle en tp_new.

También cambiamos el nombre de la función de inicialización del módulo y el nombre del módulo en la función de inicialización, como lo hicimos antes, y agregamos una definición adicional al archivo setup.py.

2.4. Apoyo a la recolección de basura cíclica

Python tiene un recolector de basura cíclico (GC) que puede identificar objetos innecesarios incluso cuando sus recuentos de referencia no son cero. Esto puede suceder cuando los objetos están involucrados en ciclos. Por ejemplo, considere:

>>> l = []
>>> l.append(l)
>>> del l

En este ejemplo, creamos una lista que se contiene a sí misma. Cuando lo eliminamos, todavía tiene una referencia de sí mismo. Su recuento de referencia no cae a cero. Afortunadamente, el recolector cíclico de basura de Python finalmente descubrirá que la lista es basura y la liberará.

En la segunda versión del ejemplo Custom, permitimos que cualquier tipo de objeto se almacene en first o last atributos 4. Además, en la segunda y tercera versión, permitimos subclases Custom, y las subclases pueden agregar atributos arbitrarios. Por cualquiera de esos dos motivos, los objetos Custom pueden participar en ciclos:

>>> import custom3
>>> class Derived(custom3.Custom): pass
...
>>> n = Derived()
>>> n.some_attribute = n

Para permitir que una instancia de Custom que participa en un ciclo de referencia sea detectada y recolectada correctamente por el GC cíclico, nuestro tipo Custom necesita llenar dos espacios adicionales y habilitar un indicador que permita estos espacios:

#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include "structmember.h"

typedef struct {
    PyObject_HEAD
    PyObject *first; /* first name */
    PyObject *last;  /* last name */
    int number;
} CustomObject;

static int
Custom_traverse(CustomObject *self, visitproc visit, void *arg)
{
    Py_VISIT(self->first);
    Py_VISIT(self->last);
    return 0;
}

static int
Custom_clear(CustomObject *self)
{
    Py_CLEAR(self->first);
    Py_CLEAR(self->last);
    return 0;
}

static void
Custom_dealloc(CustomObject *self)
{
    PyObject_GC_UnTrack(self);
    Custom_clear(self);
    Py_TYPE(self)->tp_free((PyObject *) self);
}

static PyObject *
Custom_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    CustomObject *self;
    self = (CustomObject *) type->tp_alloc(type, 0);
    if (self != NULL) {
        self->first = PyUnicode_FromString("");
        if (self->first == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->last = PyUnicode_FromString("");
        if (self->last == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->number = 0;
    }
    return (PyObject *) self;
}

static int
Custom_init(CustomObject *self, PyObject *args, PyObject *kwds)
{
    static char *kwlist[] = {"first", "last", "number", NULL};
    PyObject *first = NULL, *last = NULL, *tmp;

    if (!PyArg_ParseTupleAndKeywords(args, kwds, "|UUi", kwlist,
                                     &first, &last,
                                     &self->number))
        return -1;

    if (first) {
        tmp = self->first;
        Py_INCREF(first);
        self->first = first;
        Py_DECREF(tmp);
    }
    if (last) {
        tmp = self->last;
        Py_INCREF(last);
        self->last = last;
        Py_DECREF(tmp);
    }
    return 0;
}

static PyMemberDef Custom_members[] = {
    {"number", T_INT, offsetof(CustomObject, number), 0,
     "custom number"},
    {NULL}  /* Sentinel */
};

static PyObject *
Custom_getfirst(CustomObject *self, void *closure)
{
    Py_INCREF(self->first);
    return self->first;
}

static int
Custom_setfirst(CustomObject *self, PyObject *value, void *closure)
{
    if (value == NULL) {
        PyErr_SetString(PyExc_TypeError, "Cannot delete the first attribute");
        return -1;
    }
    if (!PyUnicode_Check(value)) {
        PyErr_SetString(PyExc_TypeError,
                        "The first attribute value must be a string");
        return -1;
    }
    Py_INCREF(value);
    Py_CLEAR(self->first);
    self->first = value;
    return 0;
}

static PyObject *
Custom_getlast(CustomObject *self, void *closure)
{
    Py_INCREF(self->last);
    return self->last;
}

static int
Custom_setlast(CustomObject *self, PyObject *value, void *closure)
{
    if (value == NULL) {
        PyErr_SetString(PyExc_TypeError, "Cannot delete the last attribute");
        return -1;
    }
    if (!PyUnicode_Check(value)) {
        PyErr_SetString(PyExc_TypeError,
                        "The last attribute value must be a string");
        return -1;
    }
    Py_INCREF(value);
    Py_CLEAR(self->last);
    self->last = value;
    return 0;
}

static PyGetSetDef Custom_getsetters[] = {
    {"first", (getter) Custom_getfirst, (setter) Custom_setfirst,
     "first name", NULL},
    {"last", (getter) Custom_getlast, (setter) Custom_setlast,
     "last name", NULL},
    {NULL}  /* Sentinel */
};

static PyObject *
Custom_name(CustomObject *self, PyObject *Py_UNUSED(ignored))
{
    return PyUnicode_FromFormat("%S %S", self->first, self->last);
}

static PyMethodDef Custom_methods[] = {
    {"name", (PyCFunction) Custom_name, METH_NOARGS,
     "Return the name, combining the first and last name"
    },
    {NULL}  /* Sentinel */
};

static PyTypeObject CustomType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "custom4.Custom",
    .tp_doc = "Custom objects",
    .tp_basicsize = sizeof(CustomObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
    .tp_new = Custom_new,
    .tp_init = (initproc) Custom_init,
    .tp_dealloc = (destructor) Custom_dealloc,
    .tp_traverse = (traverseproc) Custom_traverse,
    .tp_clear = (inquiry) Custom_clear,
    .tp_members = Custom_members,
    .tp_methods = Custom_methods,
    .tp_getset = Custom_getsetters,
};

static PyModuleDef custommodule = {
    PyModuleDef_HEAD_INIT,
    .m_name = "custom4",
    .m_doc = "Example module that creates an extension type.",
    .m_size = -1,
};

PyMODINIT_FUNC
PyInit_custom4(void)
{
    PyObject *m;
    if (PyType_Ready(&CustomType) < 0)
        return NULL;

    m = PyModule_Create(&custommodule);
    if (m == NULL)
        return NULL;

    Py_INCREF(&CustomType);
    if (PyModule_AddObject(m, "Custom", (PyObject *) &CustomType) < 0) {
        Py_DECREF(&CustomType);
        Py_DECREF(m);
        return NULL;
    }

    return m;
}

Primero, el método transversal permite que el GC cíclico conozca los subobjetos que podrían participar en los ciclos:

static int
Custom_traverse(CustomObject *self, visitproc visit, void *arg)
{
    int vret;
    if (self->first) {
        vret = visit(self->first, arg);
        if (vret != 0)
            return vret;
    }
    if (self->last) {
        vret = visit(self->last, arg);
        if (vret != 0)
            return vret;
    }
    return 0;
}

Para cada subobjeto que puede participar en ciclos, necesitamos llamar a la función visit(), que se pasa al método transversal. La función visit() toma como argumentos el subobjeto y el argumento extra arg pasados al método transversal. Retorna un valor entero que debe retornarse si no es cero.

Python proporciona una macro Py_VISIT() que automatiza las funciones de visita de llamada. Con Py_VISIT(), podemos minimizar la cantidad de repeticiones en Custom_traverse:

static int
Custom_traverse(CustomObject *self, visitproc visit, void *arg)
{
    Py_VISIT(self->first);
    Py_VISIT(self->last);
    return 0;
}

Nota

La implementación tp_traverse debe nombrar sus argumentos exactamente visit y arg para usar Py_VISIT().

En segundo lugar, debemos proporcionar un método para borrar cualquier subobjeto que pueda participar en los ciclos:

static int
Custom_clear(CustomObject *self)
{
    Py_CLEAR(self->first);
    Py_CLEAR(self->last);
    return 0;
}

Observe el uso de la macro Py_CLEAR(). Es la forma recomendada y segura de borrar los atributos de datos de tipos arbitrarios al tiempo que disminuye sus recuentos de referencia. Si tuviera que llamar a Py_XDECREF() en lugar del atributo antes de establecerlo en NULL, existe la posibilidad de que el destructor del atributo vuelva a llamar al código que lee el atributo nuevamente (especialmente si hay un ciclo de referencia).

Nota

Puede emular Py_CLEAR() escribiendo:

PyObject *tmp;
tmp = self->first;
self->first = NULL;
Py_XDECREF(tmp);

Sin embargo, es mucho más fácil y menos propenso a errores usar siempre Py_CLEAR() al eliminar un atributo. ¡No intentes micro-optimizar a expensas de la robustez!

El desasignador Custom_dealloc puede llamar a un código arbitrario al borrar los atributos. Significa que el GC circular se puede activar dentro de la función. Dado que el GC asume que el recuento de referencias no es cero, debemos destrabar el objeto del GC llamando a PyObject_GC_UnTrack() antes de borrar los miembros. Aquí está nuestro reubicador reimplementado usando PyObject_GC_UnTrack() y Custom_clear:

static void
Custom_dealloc(CustomObject *self)
{
    PyObject_GC_UnTrack(self);
    Custom_clear(self);
    Py_TYPE(self)->tp_free((PyObject *) self);
}

Finalmente, agregamos el indicador Py_TPFLAGS_HAVE_GC a los indicadores de clase:

.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,

Eso es prácticamente todo. Si hubiéramos escrito controladores personalizados tp_alloc o tp_free, tendríamos que modificarlos para la recolección de basura cíclica. La mayoría de las extensiones usarán las versiones proporcionadas automáticamente.

2.5. Subclases de otros tipos

Es posible crear nuevos tipos de extensión que se derivan de los tipos existentes. Es más fácil heredar de los tipos incorporados, ya que una extensión puede usar fácilmente PyTypeObject que necesita. Puede ser difícil compartir estas estructuras PyTypeObject entre módulos de extensión.

En este ejemplo crearemos un tipo SubList que hereda del tipo incorporado list. El nuevo tipo será completamente compatible con las listas regulares, pero tendrá un método adicional increment() que aumenta un contador interno:

>>> import sublist
>>> s = sublist.SubList(range(3))
>>> s.extend(s)
>>> print(len(s))
6
>>> print(s.increment())
1
>>> print(s.increment())
2
#define PY_SSIZE_T_CLEAN
#include <Python.h>

typedef struct {
    PyListObject list;
    int state;
} SubListObject;

static PyObject *
SubList_increment(SubListObject *self, PyObject *unused)
{
    self->state++;
    return PyLong_FromLong(self->state);
}

static PyMethodDef SubList_methods[] = {
    {"increment", (PyCFunction) SubList_increment, METH_NOARGS,
     PyDoc_STR("increment state counter")},
    {NULL},
};

static int
SubList_init(SubListObject *self, PyObject *args, PyObject *kwds)
{
    if (PyList_Type.tp_init((PyObject *) self, args, kwds) < 0)
        return -1;
    self->state = 0;
    return 0;
}

static PyTypeObject SubListType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "sublist.SubList",
    .tp_doc = "SubList objects",
    .tp_basicsize = sizeof(SubListObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
    .tp_init = (initproc) SubList_init,
    .tp_methods = SubList_methods,
};

static PyModuleDef sublistmodule = {
    PyModuleDef_HEAD_INIT,
    .m_name = "sublist",
    .m_doc = "Example module that creates an extension type.",
    .m_size = -1,
};

PyMODINIT_FUNC
PyInit_sublist(void)
{
    PyObject *m;
    SubListType.tp_base = &PyList_Type;
    if (PyType_Ready(&SubListType) < 0)
        return NULL;

    m = PyModule_Create(&sublistmodule);
    if (m == NULL)
        return NULL;

    Py_INCREF(&SubListType);
    if (PyModule_AddObject(m, "SubList", (PyObject *) &SubListType) < 0) {
        Py_DECREF(&SubListType);
        Py_DECREF(m);
        return NULL;
    }

    return m;
}

Como puede ver, el código fuente se parece mucho a los ejemplos Custom en secciones anteriores. Desglosaremos las principales diferencias entre ellos.

typedef struct {
    PyListObject list;
    int state;
} SubListObject;

La diferencia principal para los objetos de tipo derivado es que la estructura de objeto del tipo base debe ser el primer valor. El tipo base ya incluirá PyObject_HEAD() al comienzo de su estructura.

Cuando un objeto Python es una instancia de SubList, su puntero PyObject * se puede convertir de forma segura tanto en PyListObject * como en SubListObject *:

static int
SubList_init(SubListObject *self, PyObject *args, PyObject *kwds)
{
    if (PyList_Type.tp_init((PyObject *) self, args, kwds) < 0)
        return -1;
    self->state = 0;
    return 0;
}

Vemos arriba cómo llamar al método __init__ del tipo base.

Este patrón es importante cuando se escribe un tipo con miembros personalizados tp_new y tp_dealloc. El manejador tp_new no debería crear realmente la memoria para el objeto con su tp_alloc, pero deja que la clase base lo maneje llamando a su propio tp_new.

La estructura PyTypeObject admite a tp_base especificando la clase base concreta del tipo. Debido a problemas de compilación multiplataforma, no puede llenar ese campo directamente con una referencia a PyList_Type; debe hacerse más tarde en la función de inicialización del módulo:

PyMODINIT_FUNC
PyInit_sublist(void)
{
    PyObject* m;
    SubListType.tp_base = &PyList_Type;
    if (PyType_Ready(&SubListType) < 0)
        return NULL;

    m = PyModule_Create(&sublistmodule);
    if (m == NULL)
        return NULL;

    Py_INCREF(&SubListType);
    if (PyModule_AddObject(m, "SubList", (PyObject *) &SubListType) < 0) {
        Py_DECREF(&SubListType);
        Py_DECREF(m);
        return NULL;
    }

    return m;
}

Antes de llamar a PyType_Ready(), la estructura de tipo debe tener el espacio tp_base rellenado. Cuando derivamos un tipo existente, no es necesario completar el tp_alloc ranura con PyType_GenericNew() – la función de asignación del tipo base será heredada.

Después de eso, llamar a PyType_Ready() y agregar el objeto tipo al módulo es lo mismo que con los ejemplos básicos Custom.

Notas al pie

1

Esto es cierto cuando sabemos que el objeto es un tipo básico, como una cadena o un flotador.

2

Nos basamos en esto en el manejador tp_dealloc en este ejemplo, porque nuestro tipo no admite la recolección de basura.

3

Ahora sabemos que el primer y el último miembro son cadenas de caracteres, por lo que quizás podríamos ser menos cuidadosos al disminuir sus recuentos de referencia, sin embargo, aceptamos instancias de subclases de cadenas. A pesar de que la desasignación de cadenas normales no volverá a llamar a nuestros objetos, no podemos garantizar que la desasignación de una instancia de una subclase de cadena de caracteres no vuelva a llamar a nuestros objetos.

4

Además, incluso con nuestros atributos restringidos a instancias de cadenas, el usuario podría pasar subclases arbitrarias str y, por lo tanto, seguir creando ciclos de referencia.