3. Definición de tipos de extensión: temas variados

Esta sección tiene como objetivo dar un vistazo rápido a los diversos métodos de tipo que puede implementar y lo que hacen.

Aquí está la definición de PyTypeObject, con algunos campos solo utilizados en las versiones de depuración omitidas:

typedef struct _typeobject {
    PyObject_VAR_HEAD
    const char *tp_name; /* For printing, in format "<module>.<name>" */
    Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */

    /* Methods to implement standard operations */

    destructor tp_dealloc;
    Py_ssize_t tp_vectorcall_offset;
    getattrfunc tp_getattr;
    setattrfunc tp_setattr;
    PyAsyncMethods *tp_as_async; /* formerly known as tp_compare (Python 2)
                                    or tp_reserved (Python 3) */
    reprfunc tp_repr;

    /* Method suites for standard classes */

    PyNumberMethods *tp_as_number;
    PySequenceMethods *tp_as_sequence;
    PyMappingMethods *tp_as_mapping;

    /* More standard operations (here for binary compatibility) */

    hashfunc tp_hash;
    ternaryfunc tp_call;
    reprfunc tp_str;
    getattrofunc tp_getattro;
    setattrofunc tp_setattro;

    /* Functions to access object as input/output buffer */
    PyBufferProcs *tp_as_buffer;

    /* Flags to define presence of optional/expanded features */
    unsigned long tp_flags;

    const char *tp_doc; /* Documentation string */

    /* call function for all accessible objects */
    traverseproc tp_traverse;

    /* delete references to contained objects */
    inquiry tp_clear;

    /* rich comparisons */
    richcmpfunc tp_richcompare;

    /* weak reference enabler */
    Py_ssize_t tp_weaklistoffset;

    /* Iterators */
    getiterfunc tp_iter;
    iternextfunc tp_iternext;

    /* Attribute descriptor and subclassing stuff */
    struct PyMethodDef *tp_methods;
    struct PyMemberDef *tp_members;
    struct PyGetSetDef *tp_getset;
    struct _typeobject *tp_base;
    PyObject *tp_dict;
    descrgetfunc tp_descr_get;
    descrsetfunc tp_descr_set;
    Py_ssize_t tp_dictoffset;
    initproc tp_init;
    allocfunc tp_alloc;
    newfunc tp_new;
    freefunc tp_free; /* Low-level free-memory routine */
    inquiry tp_is_gc; /* For PyObject_IS_GC */
    PyObject *tp_bases;
    PyObject *tp_mro; /* method resolution order */
    PyObject *tp_cache;
    PyObject *tp_subclasses;
    PyObject *tp_weaklist;
    destructor tp_del;

    /* Type attribute cache version tag. Added in version 2.6 */
    unsigned int tp_version_tag;

    destructor tp_finalize;

} PyTypeObject;

Esos son muchos métodos. Sin embargo, no se preocupe demasiado: si tiene un tipo que desea definir, es muy probable que solo implemente un puñado de estos.

Como probablemente espera ahora, vamos a repasar esto y daremos más información sobre los diversos controladores. No iremos en el orden en que se definen en la estructura, porque hay mucho equipaje histórico que afecta el orden de los campos. A menudo es más fácil encontrar un ejemplo que incluya los campos que necesita y luego cambiar los valores para adaptarlos a su nuevo tipo.

const char *tp_name; /* For printing */

El nombre del tipo – como se mencionó en el capítulo anterior, aparecerá en varios lugares, casi por completo para fines de diagnóstico. ¡Intente elegir algo que sea útil en tal situación!

Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */

Estos campos le dicen al tiempo de ejecución cuánta memoria asignar cuando se crean nuevos objetos de este tipo. Python tiene algún soporte incorporado para estructuras de longitud variable (piense: cadenas, tuplas) que es donde entra el campo tp_itemsize. Esto se tratará más adelante.

const char *tp_doc;

Aquí puede poner una cadena de caracteres (o su dirección) que desea que se retorne cuando el script de Python haga referencia a obj.__doc__ para recuperar el docstring.

Ahora llegamos a los métodos de tipo básicos: los que implementarán la mayoría de los tipos de extensión.

3.1. Finalización y desasignación

destructor tp_dealloc;

Se llama a esta función cuando el recuento de referencia de la instancia de su tipo se reduce a cero y el intérprete de Python quiere reclamarlo. Si su tipo tiene memoria para liberar u otra limpieza para realizar, puede ponerla aquí. El objeto en sí mismo necesita ser liberado aquí también. Aquí hay un ejemplo de esta función:

static void
newdatatype_dealloc(newdatatypeobject *obj)
{
    free(obj->obj_UnderlyingDatatypePtr);
    Py_TYPE(obj)->tp_free(obj);
}

Un requisito importante de la función desasignador es que deja solo las excepciones pendientes. Esto es importante ya que los desasignadores se llaman con frecuencia cuando el intérprete desenrolla la pila de Python; cuando la pila se desenrolla debido a una excepción (en lugar de retornos normales), no se hace nada para proteger a los desasignadores de memoria (deallocator) de ver que ya se ha establecido una excepción. Cualquier acción que realice un desasignador que pueda hacer que se ejecute código Python adicional puede detectar que se ha establecido una excepción. Esto puede conducir a errores engañosos del intérprete. La forma correcta de protegerse contra esto es guardar una excepción pendiente antes de realizar la acción insegura y restaurarla cuando haya terminado. Esto se puede hacer usando las funciones PyErr_Fetch() y PyErr_Restore():

static void
my_dealloc(PyObject *obj)
{
    MyObject *self = (MyObject *) obj;
    PyObject *cbresult;

    if (self->my_callback != NULL) {
        PyObject *err_type, *err_value, *err_traceback;

        /* This saves the current exception state */
        PyErr_Fetch(&err_type, &err_value, &err_traceback);

        cbresult = PyObject_CallObject(self->my_callback, NULL);
        if (cbresult == NULL)
            PyErr_WriteUnraisable(self->my_callback);
        else
            Py_DECREF(cbresult);

        /* This restores the saved exception state */
        PyErr_Restore(err_type, err_value, err_traceback);

        Py_DECREF(self->my_callback);
    }
    Py_TYPE(obj)->tp_free((PyObject*)self);
}

Nota

Existen limitaciones para lo que puede hacer de manera segura en una función de desasignación. Primero, si su tipo admite la recolección de basura (usando tp_traverse o tp_clear), algunos de los miembros del objeto pueden haber sido borrados o finalizados por el time tp_dealloc es llamado. Segundo, en tp_dealloc, su objeto está en un estado inestable: su recuento de referencia es igual a cero. Cualquier llamada a un objeto o API no trivial (como en el ejemplo anterior) podría terminar llamando tp_dealloc nuevamente, causando una doble liberación y un bloqueo.

Comenzando con Python 3.4, se recomienda no poner ningún código de finalización complejo en tp_dealloc, y en su lugar use el nuevo método de tipo tp_finalize.

Ver también

PEP 442 explica el nuevo esquema de finalización.

3.2. Presentación de Objetos

En Python, hay dos formas de generar una representación textual de un objeto: la función repr(), y la función str(). (La función print() solo llama a str().) Estos controladores son opcionales.

reprfunc tp_repr;
reprfunc tp_str;

El manejador tp_repr debe retornar un objeto de cadena que contenga una representación de la instancia para la que se llama. Aquí hay un ejemplo simple:

static PyObject *
newdatatype_repr(newdatatypeobject * obj)
{
    return PyUnicode_FromFormat("Repr-ified_newdatatype{{size:%d}}",
                                obj->obj_UnderlyingDatatypePtr->size);
}

Si no se especifica tp_repr, el intérprete proporcionará una representación que utiliza los tipos tp_name y un valor de identificación único para el objeto.

El manejador tp_str es para str() lo que el manejador tp_repr descrito arriba es para repr(); es decir, se llama cuando el código Python llama str() en una instancia de su objeto. Su implementación es muy similar a la función tp_repr, pero la cadena resultante está destinada al consumo humano. Si tp_str no se especifica, en su lugar se utiliza el controlador tp_repr.

Aquí hay un ejemplo simple:

static PyObject *
newdatatype_str(newdatatypeobject * obj)
{
    return PyUnicode_FromFormat("Stringified_newdatatype{{size:%d}}",
                                obj->obj_UnderlyingDatatypePtr->size);
}

3.3. Gestión de atributos

Para cada objeto que puede soportar atributos, el tipo correspondiente debe proporcionar las funciones que controlan cómo se resuelven los atributos. Es necesario que haya una función que pueda recuperar atributos (si hay alguna definida), y otra para establecer atributos (si se permite establecer atributos). La eliminación de un atributo es un caso especial, para el cual el nuevo valor pasado al controlador es NULL.

Python admite dos pares de controladores de atributos; un tipo que admite atributos solo necesita implementar las funciones para un par. La diferencia es que un par toma el nombre del atributo como a char*, mientras que el otro acepta un PyObject*. Cada tipo puede usar el par que tenga más sentido para la conveniencia de la implementación.

getattrfunc  tp_getattr;        /* char * version */
setattrfunc  tp_setattr;
/* ... */
getattrofunc tp_getattro;       /* PyObject * version */
setattrofunc tp_setattro;

Si acceder a los atributos de un objeto es siempre una operación simple (esto se explicará en breve), existen implementaciones genéricas que se pueden utilizar para proporcionar la versión PyObject* de las funciones de gestión de atributos. La necesidad real de controladores de atributos específicos de tipo desapareció casi por completo a partir de Python 2.2, aunque hay muchos ejemplos que no se han actualizado para utilizar algunos de los nuevos mecanismos genéricos que están disponibles.

3.3.1. Gestión de atributos genéricos

La mayoría de los tipos de extensión solo usan atributos simple. Entonces, ¿qué hace que los atributos sean simples? Solo hay un par de condiciones que se deben cumplir:

  1. El nombre de los atributos debe ser conocido cuando PyType_Ready() es llamado.

  2. No se necesita un procesamiento especial para registrar que un atributo se buscó o se configuró, ni se deben tomar acciones basadas en el valor.

Tenga en cuenta que esta lista no impone restricciones a los valores de los atributos, cuándo se calculan los valores o cómo se almacenan los datos relevantes.

Cuando se llama a PyType_Ready(), utiliza tres tablas a las que hace referencia el objeto de tipo para crear descriptor que se colocan en el diccionario del objeto de tipo. Cada descriptor controla el acceso a un atributo del objeto de instancia. Cada una de las tablas es opcional; si los tres son NULL, las instancias del tipo solo tendrán atributos que se heredan de su tipo base, y deberían dejar tp_getattro y los campos tp_setattro NULL también, permitiendo que el tipo base maneje los atributos.

Las tablas se declaran como tres campos del tipo objeto:

struct PyMethodDef *tp_methods;
struct PyMemberDef *tp_members;
struct PyGetSetDef *tp_getset;

Si tp_methods no es NULL, debe referirse a un arreglo de estructuras PyMethodDef. Cada entrada en la tabla es una instancia de esta estructura:

typedef struct PyMethodDef {
    const char  *ml_name;       /* method name */
    PyCFunction  ml_meth;       /* implementation function */
    int          ml_flags;      /* flags */
    const char  *ml_doc;        /* docstring */
} PyMethodDef;

Se debe definir una entrada para cada método proporcionado por el tipo; No se necesitan entradas para los métodos heredados de un tipo base. Se necesita una entrada adicional al final; es un centinela el que marca el final del arreglo. El campo ml_name del centinela debe ser NULL.

La segunda tabla se utiliza para definir atributos que se asignan directamente a los datos almacenados en la instancia. Se admite una variedad de tipos C primitivos, y el acceso puede ser de solo lectura o lectura-escritura. Las estructuras en la tabla se definen como:

typedef struct PyMemberDef {
    const char *name;
    int         type;
    int         offset;
    int         flags;
    const char *doc;
} PyMemberDef;

Para cada entrada en la tabla, se construirá un descriptor y se agregará al tipo que podrá extraer un valor de la estructura de la instancia. El campo type debe contener uno de los códigos de tipo definidos en el encabezado structmember.h; el valor se usará para determinar cómo convertir los valores de Python hacia y desde los valores de C. El campo flags se usa para almacenar flags que controlan cómo se puede acceder al atributo.

Las siguientes constantes de flag se definen en structmember.h; se pueden combinar usando OR bit a bit (bitwise-OR).

Constante

Significado

READONLY

Nunca escribible.

READ_RESTRICTED

No legible en modo restringido.

WRITE_RESTRICTED

No se puede escribir en modo restringido.

RESTRICTED

No se puede leer ni escribir en modo restringido.

Una ventaja interesante de usar la tabla tp_members para crear descriptores que se usan en tiempo de ejecución es que cualquier atributo definido de esta manera puede tener un docstring asociada simplemente al proporcionar el texto en la tabla. Una aplicación puede usar la API de introspección para recuperar el descriptor del objeto de clase y obtener el docstring utilizando su atributo __doc__.

Al igual que con la tabla tp_methods, se requiere una entrada de centinela con un valor name de NULL.

3.3.2. Gestión de atributos específicos de tipo

Para simplificar, aquí solo se demostrará la versión char *; el tipo de parámetro de nombre es la única diferencia entre las variaciones de la interfaz char* y PyObject*. Este ejemplo efectivamente hace lo mismo que el ejemplo genérico anterior, pero no utiliza el soporte genérico agregado en Python 2.2. Explica cómo se llaman las funciones del controlador, de modo que si necesita ampliar su funcionalidad, comprenderá lo que debe hacerse.

Se llama al manejador tp_getattr cuando el objeto requiere una búsqueda de atributo. Se llama en las mismas situaciones donde se llamaría el método __getattr__() de una clase.

Aquí hay un ejemplo:

static PyObject *
newdatatype_getattr(newdatatypeobject *obj, char *name)
{
    if (strcmp(name, "data") == 0)
    {
        return PyLong_FromLong(obj->data);
    }

    PyErr_Format(PyExc_AttributeError,
                 "'%.50s' object has no attribute '%.400s'",
                 tp->tp_name, name);
    return NULL;
}

Se llama al manejador tp_setattr cuando se llama al método __setattr__() o __delattr__() de una instancia de clase. Cuando se debe eliminar un atributo, el tercer parámetro será NULL. Aquí hay un ejemplo que simplemente plantea una excepción; si esto fuera realmente todo lo que deseaba, el controlador tp_setattr debería establecerse en NULL.

static int
newdatatype_setattr(newdatatypeobject *obj, char *name, PyObject *v)
{
    PyErr_Format(PyExc_RuntimeError, "Read-only attribute: %s", name);
    return -1;
}

3.4. Comparación de Objetos

richcmpfunc tp_richcompare;

Se llama al manejador tp_richcompare cuando se necesitan comparaciones. Es análogo a métodos de comparación ricos, como __lt__(), y también llamado por PyObject_RichCompare() y PyObject_RichCompareBool().

Esta función se llama con dos objetos Python y el operador como argumentos, donde el operador es uno de Py_EQ, Py_NE, Py_LE, Py_GT, Py_LT o Py_GT. Debe comparar los dos objetos con respecto al operador especificado y retornar Py_True o Py_False si la comparación es exitosa, Py_NotImplemented para indicar que la comparación no está implementada y el método de comparación del otro objeto debería intentarse, o NULL si se estableció una excepción.

Aquí hay una implementación de muestra, para un tipo de datos que se considera igual si el tamaño de un puntero interno es igual:

static PyObject *
newdatatype_richcmp(PyObject *obj1, PyObject *obj2, int op)
{
    PyObject *result;
    int c, size1, size2;

    /* code to make sure that both arguments are of type
       newdatatype omitted */

    size1 = obj1->obj_UnderlyingDatatypePtr->size;
    size2 = obj2->obj_UnderlyingDatatypePtr->size;

    switch (op) {
    case Py_LT: c = size1 <  size2; break;
    case Py_LE: c = size1 <= size2; break;
    case Py_EQ: c = size1 == size2; break;
    case Py_NE: c = size1 != size2; break;
    case Py_GT: c = size1 >  size2; break;
    case Py_GE: c = size1 >= size2; break;
    }
    result = c ? Py_True : Py_False;
    Py_INCREF(result);
    return result;
 }

3.5. Soporte de protocolo abstracto

Python admite una variedad de protocolos abstractos; las interfaces específicas proporcionadas para usar estas interfaces están documentadas en Capa de objetos abstractos.

Varias de estas interfaces abstractas se definieron temprano en el desarrollo de la implementación de Python. En particular, los protocolos de número, mapeo y secuencia han sido parte de Python desde el principio. Se han agregado otros protocolos con el tiempo. Para los protocolos que dependen de varias rutinas de controlador de la implementación de tipo, los protocolos más antiguos se han definido como bloques opcionales de controladores a los que hace referencia el objeto de tipo. Para los protocolos más nuevos, hay espacios adicionales en el objeto de tipo principal, con un bit de marca que se establece para indicar que los espacios están presentes y el intérprete debe verificarlos. (El bit de indicador no indica que los valores de intervalo no son NULL. El indicador puede establecerse para indicar la presencia de un intervalo, pero un intervalo aún puede estar vacío.):

PyNumberMethods   *tp_as_number;
PySequenceMethods *tp_as_sequence;
PyMappingMethods  *tp_as_mapping;

Si desea que su objeto pueda actuar como un número, una secuencia o un objeto de mapeo, entonces coloca la dirección de una estructura que implementa el tipo C PyNumberMethods, PySequenceMethods, o PyMappingMethods, respectivamente. Depende de usted completar esta estructura con los valores apropiados. Puede encontrar ejemplos del uso de cada uno de estos en el directorio Objects de la distribución fuente de Python.

hashfunc tp_hash;

Esta función, si elige proporcionarla, debería retornar un número hash para una instancia de su tipo de datos. Aquí hay un ejemplo simple:

static Py_hash_t
newdatatype_hash(newdatatypeobject *obj)
{
    Py_hash_t result;
    result = obj->some_size + 32767 * obj->some_number;
    if (result == -1)
       result = -2;
    return result;
}

Py_hash_t es un tipo entero con signo con un ancho que varia dependiendo de la plataforma.retornar -1 de tp_hash indica un error, por lo que debe tener cuidado de evitar retornarlo cuando el cálculo de hash sea exitoso, como se vio anteriormente.

ternaryfunc tp_call;

Esta función se llama cuando una instancia de su tipo de datos se «llama», por ejemplo, si obj1 es una instancia de su tipo de datos y el script de Python contiene obj1('hello'), el controlador tp_call se invoca.

Esta función toma tres argumentos:

  1. self es la instancia del tipo de datos que es el sujeto de la llamada. Si la llamada es obj1('hola'), entonces self es obj1.

  2. args es una tupla que contiene los argumentos de la llamada. Puede usar PyArg_ParseTuple() para extraer los argumentos.

  3. kwds es un diccionario de argumentos de palabras clave que se pasaron. Si no es NULL y admite argumentos de palabras clave, use PyArg_ParseTupleAndKeywords() para extraer los argumentos. Si no desea admitir argumentos de palabras clave y esto no es NULL, genere un TypeError con un mensaje que indique que los argumentos de palabras clave no son compatibles.

Aquí hay una implementación de juguete tp_call:

static PyObject *
newdatatype_call(newdatatypeobject *self, PyObject *args, PyObject *kwds)
{
    PyObject *result;
    const char *arg1;
    const char *arg2;
    const char *arg3;

    if (!PyArg_ParseTuple(args, "sss:call", &arg1, &arg2, &arg3)) {
        return NULL;
    }
    result = PyUnicode_FromFormat(
        "Returning -- value: [%d] arg1: [%s] arg2: [%s] arg3: [%s]\n",
        obj->obj_UnderlyingDatatypePtr->size,
        arg1, arg2, arg3);
    return result;
}
/* Iterators */
getiterfunc tp_iter;
iternextfunc tp_iternext;

Estas funciones proporcionan soporte para el protocolo iterador. Ambos manejadores toman exactamente un parámetro, la instancia para la que están siendo llamados, y retornan una nueva referencia. En el caso de un error, deben establecer una excepción y retornar NULL. tp_iter corresponde al método Python __iter__(), mientras que tp_iternext corresponde al método Python __next__().

Cualquier objeto iterable debe implementar el manejador tp_iter, que debe retornar un objeto iterator. Aquí se aplican las mismas pautas que para las clases de Python:

  • Para colecciones (como listas y tuplas) que pueden admitir múltiples iteradores independientes, cada llamada debe crear y retornar un nuevo iterador a tp_iter.

  • Los objetos que solo se pueden iterar una vez (generalmente debido a los efectos secundarios de la iteración, como los objetos de archivo) pueden implementar tp_iter retornando una nueva referencia a ellos mismos y, por lo tanto, también deben implementar el manejador tp_iternext.

Cualquier objeto iterator debe implementar tanto tp_iter como tp_iternext. El manejador de un iterador tp_iter debería retornar una nueva referencia al iterador. Su controlador tp_iternext debería retornar una nueva referencia al siguiente objeto en la iteración, si hay uno. Si la iteración ha llegado al final, tp_iternext puede retornar NULL sin establecer una excepción, o puede establecer StopIteration además para retornar NULL; evitar la excepción puede producir un rendimiento ligeramente mejor. Si se produce un error real, tp_iternext siempre debe establecer una excepción y retornar NULL.

3.6. Soporte de referencia débil

Uno de los objetivos de la implementación de referencia débil de Python es permitir que cualquier tipo participe en el mecanismo de referencia débil sin incurrir en la sobrecarga de objetos críticos para el rendimiento (como los números).

Ver también

Documentación para el módulo weakref.

Para que un objeto sea débilmente referenciable, el tipo de extensión debe hacer dos cosas:

  1. Incluya el campo a PyObject* en la estructura del objeto C dedicada al mecanismo de referencia débil. El constructor del objeto debe dejarlo NULL (que es automático cuando se usa el valor predeterminado tp_alloc).

  2. Establezca el miembro de tipo tp_weaklistoffset en el desplazamiento del campo mencionado anteriormente en la estructura del objeto C, para que el intérprete sepa cómo acceder y modificar ese campo.

Concretamente, así es como una estructura de objeto trivial se aumentaría con el campo requerido:

typedef struct {
    PyObject_HEAD
    PyObject *weakreflist;  /* List of weak references */
} TrivialObject;

Y el miembro correspondiente en el objeto de tipo declarado estáticamente:

static PyTypeObject TrivialType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    /* ... other members omitted for brevity ... */
    .tp_weaklistoffset = offsetof(TrivialObject, weakreflist),
};

La única adición adicional es que tp_dealloc necesita borrar cualquier referencia débil (llamando a PyObject_ClearWeakRefs()) si el campo no es NULL

static void
Trivial_dealloc(TrivialObject *self)
{
    /* Clear weakrefs first before calling any destructors */
    if (self->weakreflist != NULL)
        PyObject_ClearWeakRefs((PyObject *) self);
    /* ... remainder of destruction code omitted for brevity ... */
    Py_TYPE(self)->tp_free((PyObject *) self);
}

3.7. Más Sugerencias

Para aprender a implementar cualquier método específico para su nuevo tipo de datos, obtenga el código fuente CPython. Vaya al directorio: file: Objects, luego busque en los archivos fuente C tp_ más la función que desee (por ejemplo, tp_richcompare). Encontrará ejemplos de la función que desea implementar.

Cuando necesite verificar que un objeto es una instancia concreta del tipo que está implementando, use la función PyObject_TypeCheck(). Una muestra de su uso podría ser algo como lo siguiente:

if (!PyObject_TypeCheck(some_object, &MyType)) {
    PyErr_SetString(PyExc_TypeError, "arg #1 not a mything");
    return NULL;
}

Ver también

Descargue las versiones de origen de CPython.

https://www.python.org/downloads/source/

El proyecto CPython en GitHub, donde se desarrolla el código fuente de CPython.

https://github.com/python/cpython