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:
Lo que contiene un objeto
Custom
: esta es la estructuraCustomObject
, que se asigna una vez para cada instancia deCustom
.Cómo se comporta
Custom
type: esta es la estructuraCustomType
, que define un conjunto de indicadores y punteros de función que el intérprete inspecciona cuando se solicitan operaciones específicas.Cómo inicializar el módulo
custom
: esta es la funciónPyInit_custom
y la estructura asociadacustommodule
.
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.