Nota

¡Ayúdanos a traducir la documentación oficial de Python al Español! Puedes encontrar más información en Como contribuir. Ayuda a acercar Python a más personas de habla hispana.

Aislamiento de módulos de extensión

Resumen

Tradicionalmente, el estado perteneciente a los módulos de extensión de Python se mantuvo en las variables C static, que tienen un alcance de todo el proceso. Este documento describe los problemas de dicho estado por proceso y muestra una forma más segura: el estado por módulo.

El documento también describe cómo cambiar al estado por módulo cuando sea posible. Esta transición implica asignar espacio para ese estado, cambiar potencialmente de tipos estáticos a tipos de montón y, quizás lo más importante, acceder al estado por módulo desde el código.

¿Quién debería leer esto?

Esta guía está escrita para los mantenedores de extensiones C-API que deseen hacer que esa extensión sea más segura para usar en aplicaciones donde Python se usa como biblioteca.

Trasfondo

Un intérprete (interpreter) es el contexto en el que se ejecuta el código de Python. Contiene la configuración (p. ej., la ruta de importación) y el estado de tiempo de ejecución (p. ej., el conjunto de módulos importados).

Python admite la ejecución de varios intérpretes en un solo proceso. Hay dos casos en los que pensar, los usuarios pueden ejecutar intérpretes:

Ambos casos (y combinaciones de ellos) serían más útiles al incorporar Python dentro de una biblioteca. Las bibliotecas generalmente no deben hacer suposiciones sobre la aplicación que las usa, lo que incluye asumir un «intérprete principal de Python» en todo el proceso.

Históricamente, los módulos de extensión de Python no manejan bien este caso de uso. Muchos módulos de extensión (e incluso algunos módulos stdlib) usan el estado global por-proceso, porque las variables C static son extremadamente fáciles de usar. Así, los datos que deberían ser específicos de un intérprete acaban siendo compartidos entre intérpretes. A menos que el desarrollador de la extensión tenga cuidado, es muy fácil introducir casos extremos que provocan bloqueos cuando un módulo se carga en más de un intérprete en el mismo proceso.

Desafortunadamente, el estado por-intérprete no es fácil de lograr. Los autores de extensiones tienden a no tener en cuenta múltiples intérpretes cuando desarrollan, y actualmente es engorroso probar el comportamiento.

Ingrese al estado por módulo

En lugar de centrarse en el estado por intérprete, la API C de Python está evolucionando para admitir mejor el estado por-módulo más granular. Esto significa que los datos de nivel C se adjuntan a un module object. Cada intérprete crea su propio objeto de módulo, manteniendo los datos separados. Para probar el aislamiento, se pueden cargar varios objetos de módulo correspondientes a una sola extensión en un solo intérprete.

El estado por módulo proporciona una manera fácil de pensar en la vida útil y la propiedad de los recursos: el módulo de extensión se inicializará cuando se cree un objeto de módulo y se limpiará cuando se libere. En este sentido, un módulo es como cualquier otro PyObject*; no hay ganchos de «apagado del intérprete» para pensar u olvidar.

Tenga en cuenta que hay casos de uso para diferentes tipos de «globales»: por proceso, por intérprete, por subproceso o por estado de tarea. Con el estado por módulo como predeterminado, estos aún son posibles, pero debe tratarlos como casos excepcionales: si los necesita, debe brindarles atención y pruebas adicionales. (Tenga en cuenta que esta guía no los cubre).

Objetos módulos aislados

El punto clave a tener en cuenta al desarrollar un módulo de extensión es que se pueden crear varios objetos de módulo a partir de una única biblioteca compartida. Por ejemplo:

>>> import sys
>>> import binascii
>>> old_binascii = binascii
>>> del sys.modules['binascii']
>>> import binascii  # create a new module object
>>> old_binascii == binascii
False

Como regla general, los dos módulos deben ser completamente independientes. Todos los objetos y el estado específico del módulo deben encapsularse dentro del objeto del módulo, no compartirse con otros objetos del módulo y limpiarse cuando se desasigna el objeto del módulo. Dado que esto es solo una regla general, las excepciones son posibles (consulte Managing Global State), pero necesitarán más reflexión y atención en los casos extremos.

Si bien algunos módulos podrían funcionar con restricciones menos estrictas, los módulos aislados facilitan el establecimiento de expectativas y pautas claras que funcionan en una variedad de casos de uso.

Casos extremos sorprendentes

Tenga en cuenta que los módulos aislados crean algunos casos extremos sorprendentes. En particular, cada objeto de módulo normalmente no compartirá sus clases y excepciones con otros módulos similares. Continuando con example above, tenga en cuenta que old_binascii.Error y binascii.Error son objetos separados. En el código siguiente, se detecta la excepción not:

>>> old_binascii.Error == binascii.Error
False
>>> try:
...     old_binascii.unhexlify(b'qwertyuiop')
... except binascii.Error:
...     print('boo')
...
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
binascii.Error: Non-hexadecimal digit found

Esto se espera. Tenga en cuenta que los módulos de Python puro se comportan de la misma manera: es una parte de cómo funciona Python.

El objetivo es hacer que los módulos de extensión sean seguros en el nivel C, no hacer que los piratas informáticos se comporten de manera intuitiva. Mutar sys.modules «manualmente» cuenta como un hack.

Cómo hacer que los módulos sean seguros con varios intérpretes

Administrar el estado global

A veces, el estado asociado con un módulo de Python no es específico de ese módulo, sino de todo el proceso (o algo más «más global» que un módulo). Por ejemplo:

  • El módulo readline gestiona el terminal.

  • Un módulo que se ejecuta en una placa de circuito quiere controlar el LED integrado.

En estos casos, el módulo Python debería proporcionar acceso al estado global, en lugar de poseerlo. Si es posible, escriba el módulo para que varias copias del mismo puedan acceder al estado de forma independiente (junto con otras bibliotecas, ya sea para Python u otros lenguajes). Si eso no es posible, considere el bloqueo explícito.

Si es necesario usar el estado global del proceso, la forma más sencilla de evitar problemas con varios intérpretes es evitar explícitamente que un módulo se cargue más de una vez por proceso; consulte Opt-Out: Limiting to One Module Object per Process.

Administración del estado por módulo

Para usar el estado por módulo, use multi-phase extension module initialization. Esto indica que su módulo admite múltiples intérpretes correctamente.

Establezca PyModuleDef.m_size en un número positivo para solicitar tantos bytes de almacenamiento local para el módulo. Por lo general, esto se establecerá en el tamaño de algún struct específico del módulo, que puede almacenar todo el estado de nivel C del módulo. En particular, es donde debe colocar los punteros a las clases (incluidas las excepciones, pero excluyendo los tipos estáticos) y configuraciones (por ejemplo, field_size_limit de csv) que el código C necesita para funcionar.

Nota

Otra opción es almacenar el estado en el __dict__ del módulo, pero debe evitar fallas cuando los usuarios modifican __dict__ desde el código de Python. Esto generalmente significa verificación de errores y tipos en el nivel C, que es fácil equivocarse y difícil de probar lo suficiente.

Sin embargo, si el estado del módulo no es necesario en el código C, almacenarlo solo en __dict__ es una buena idea.

Si el estado del módulo incluye punteros PyObject, el objeto del módulo debe contener referencias a esos objetos e implementar los enlaces de nivel de módulo m_traverse, m_clear y m_free. Estos funcionan como tp_traverse, tp_clear y tp_free de una clase. Agregarlos requerirá algo de trabajo y hará que el código sea más largo; este es el precio de los módulos que se pueden descargar limpiamente.

Un ejemplo de un módulo con estado por módulo está actualmente disponible como xxlimited; ejemplo de inicialización del módulo que se muestra en la parte inferior del archivo.

Exclusión voluntaria: limitación a un objeto de módulo por proceso

Un PyModuleDef.m_size no negativo indica que un módulo admite varios intérpretes correctamente. Si este aún no es el caso de su módulo, puede hacer que su módulo se pueda cargar explícitamente solo una vez por proceso. Por ejemplo:

static int loaded = 0;

static int
exec_module(PyObject* module)
{
    if (loaded) {
        PyErr_SetString(PyExc_ImportError,
                        "cannot load module more than once per process");
        return -1;
    }
    loaded = 1;
    // ... rest of initialization
}

Acceso al estado del módulo desde las funciones

Acceder al estado desde funciones a nivel de módulo es sencillo. Las funciones obtienen el objeto del módulo como su primer argumento; para extraer el estado, puede usar PyModule_GetState:

static PyObject *
func(PyObject *module, PyObject *args)
{
    my_struct *state = (my_struct*)PyModule_GetState(module);
    if (state == NULL) {
        return NULL;
    }
    // ... rest of logic
}

Nota

PyModule_GetState puede retornar NULL sin establecer una excepción si no hay un estado de módulo, es decir, PyModuleDef.m_size era cero. En su propio módulo, tiene el control de m_size, por lo que es fácil de evitar.

Tipos Heap

Tradicionalmente, los tipos definidos en código C son estáticos; es decir, estructuras static PyTypeObject definidas directamente en el código e inicializadas mediante PyType_Ready().

Tales tipos son necesariamente compartidos a lo largo del proceso. Compartirlos entre objetos de módulo requiere prestar atención a cualquier estado que posean o al que accedan. Para limitar los posibles problemas, los tipos estáticos son inmutables en el nivel de Python: por ejemplo, no puede configurar str.myattribute = 123.

CPython implementation detail: Compartir objetos verdaderamente inmutables entre intérpretes está bien, siempre que no proporcionen acceso a objetos mutables. Sin embargo, en CPython, cada objeto de Python tiene un detalle de implementación mutable: el recuento de referencias. Los cambios en el refcount están protegidos por el GIL. Por lo tanto, el código que comparte cualquier objeto de Python entre intérpretes depende implícitamente del GIL actual de todo el proceso de CPython.

Debido a que son inmutables y globales de proceso, los tipos estáticos no pueden acceder a «su» estado de módulo. Si algún método de este tipo requiere acceso al estado del módulo, el tipo debe convertirse a tipo almacenado en memoria dinámica (heap) o tipo heap para abreviar. Estos se corresponden más estrechamente con las clases creadas por la instrucción class de Python.

Para los módulos nuevos, usar tipos heap de forma predeterminada es una buena regla general.

Cambio de tipos estáticos a tipos heap

Los tipos estáticos se pueden convertir en tipos heap, pero tenga en cuenta que la API de tipo heap no se diseñó para la conversión «sin pérdidas» de tipos estáticos, es decir, para crear un tipo que funcione exactamente como un tipo estático determinado. Por lo tanto, al reescribir la definición de clase en una nueva API, es probable que cambie sin querer algunos detalles (por ejemplo, capacidad de selección o espacios heredados). Siempre pruebe los detalles que son importantes para usted.

Tenga cuidado con los siguientes dos puntos en particular (pero tenga en cuenta que esta no es una lista completa):

  • A diferencia de los tipos estáticos, los objetos de tipo heap son mutables de forma predeterminada. Utilice el indicador Py_TPFLAGS_IMMUTABLETYPE para evitar la mutabilidad.

  • Los tipos heap heredan tp_new de forma predeterminada, por lo que es posible crear instancias de ellos desde el código de Python. Puede evitar esto con el indicador Py_TPFLAGS_DISALLOW_INSTANTIATION.

Definición de tipos heap

Los tipos heap se pueden crear completando una estructura PyType_Spec, una descripción o «modelo» de una clase y llamando a PyType_FromModuleAndSpec() para construir un nuevo objeto de clase.

Nota

Otras funciones, como PyType_FromSpec(), también pueden crear tipos heap, pero PyType_FromModuleAndSpec() asocia el módulo con la clase, lo que permite el acceso al estado del módulo desde los métodos.

La clase generalmente debe almacenarse en ambos, el estado del módulo (para acceso seguro desde C) y el __dict__ del módulo (para acceso desde código Python).

Protocolo de recolección de basura

Las instancias de tipos heap contienen una referencia a su tipo. Esto garantiza que el tipo no se destruya antes de que se destruyan todas sus instancias, pero puede generar ciclos de referencia que el recolector de elementos no utilizados debe interrumpir.

Para evitar pérdidas de memoria, las instancias de los tipos heap deben implementar el protocolo de recolección de elementos no utilizados. Es decir, los tipos heap deben:

  • Tener la bandera Py_TPFLAGS_HAVE_GC.

  • Defina una función transversal usando Py_tp_traverse, que visita el tipo (por ejemplo, usando Py_VISIT(Py_TYPE(self))).

Consulte el the documentation de Py_TPFLAGS_HAVE_GC y tp_traverse para obtener consideraciones adicionales.

Si su función transversal delega al tp_traverse de su clase base (u otro tipo), asegúrese de que Py_TYPE(self) se visite solo una vez. Tenga en cuenta que solo se espera que el tipo de montón visite el tipo en tp_traverse.

Por ejemplo, si su función poligonal incluye:

base->tp_traverse(self, visit, arg)

…y base puede ser un tipo estático, entonces también debe incluir:

if (base->tp_flags & Py_TPFLAGS_HEAPTYPE) {
    // a heap type's tp_traverse already visited Py_TYPE(self)
} else {
    Py_VISIT(Py_TYPE(self));
}

No es necesario manejar el recuento de referencias del tipo en tp_new y tp_clear.

Acceso al estado del módulo desde las clases

Si tiene un objeto de tipo definido con PyType_FromModuleAndSpec(), puede llamar a PyType_GetModule() para obtener el módulo asociado y luego a PyModule_GetState() para obtener el estado del módulo.

Para ahorrar un tedioso código repetitivo de manejo de errores, puede combinar estos dos pasos con PyType_GetModuleState(), lo que da como resultado:

my_struct *state = (my_struct*)PyType_GetModuleState(type);
if (state === NULL) {
    return NULL;
}

Acceso al estado del módulo desde métodos regulares

Acceder al estado de nivel de módulo desde los métodos de una clase es algo más complicado, pero es posible gracias a la API introducida en Python 3.9. Para obtener el estado, primero debe obtener la clase de definición y luego obtener el estado del módulo.

El obstáculo más grande es obtener la clase en la que se definió un método, o la «clase de definición» de ese método para abreviar. La clase de definición puede tener una referencia al módulo del que forma parte.

No confunda la clase de definición con Py_TYPE(self). Si se llama al método en una subclase de su tipo, Py_TYPE(self) se referirá a esa subclase, que puede estar definida en un módulo diferente al suyo.

Nota

El siguiente código de Python puede ilustrar el concepto. Base.get_defining_class retorna Base incluso si type(self) == Sub:

class Base:
    def get_type_of_self(self):
        return type(self)

    def get_defining_class(self):
        return __class__

class Sub(Base):
    pass

Para que un método obtenga su «clase de definición», debe usar METH_METHOD | METH_FASTCALL | METH_KEYWORDS calling convention y la firma PyCMethod correspondiente:

PyObject *PyCMethod(
    PyObject *self,               // object the method was called on
    PyTypeObject *defining_class, // defining class
    PyObject *const *args,        // C array of arguments
    Py_ssize_t nargs,             // length of "args"
    PyObject *kwnames)            // NULL, or dict of keyword arguments

Una vez que tenga la clase de definición, llame a PyType_GetModuleState() para obtener el estado de su módulo asociado.

Por ejemplo:

static PyObject *
example_method(PyObject *self,
        PyTypeObject *defining_class,
        PyObject *const *args,
        Py_ssize_t nargs,
        PyObject *kwnames)
{
    my_struct *state = (my_struct*)PyType_GetModuleState(defining_class);
    if (state === NULL) {
        return NULL;
    }
    ... // rest of logic
}

PyDoc_STRVAR(example_method_doc, "...");

static PyMethodDef my_methods[] = {
    {"example_method",
      (PyCFunction)(void(*)(void))example_method,
      METH_METHOD|METH_FASTCALL|METH_KEYWORDS,
      example_method_doc}
    {NULL},
}

Acceso al estado del módulo desde métodos de Slot, Getters y Setters

Nota

Esto es nuevo en Python 3.11.

Los métodos slot, los equivalentes rápidos de C para métodos especiales, como nb_add para __add__ o tp_new para la inicialización, tienen una API muy simple que no permite pasar la clase de definición, a diferencia de PyCMethod. Lo mismo ocurre con los getters y setters definidos con PyGetSetDef.

Para acceder al estado del módulo en estos casos, utilice la función PyType_GetModuleByDef() y pase la definición del módulo. Una vez que tenga el módulo, llame a PyModule_GetState() para obtener el estado:

PyObject *module = PyType_GetModuleByDef(Py_TYPE(self), &module_def);
my_struct *state = (my_struct*)PyModule_GetState(module);
if (state === NULL) {
    return NULL;
}

PyType_GetModuleByDef funciona buscando en method resolution order (es decir, todas las superclases) la primera superclase que tiene un módulo correspondiente.

Nota

En casos muy exóticos (cadenas de herencia que abarcan varios módulos creados a partir de la misma definición), es posible que PyType_GetModuleByDef no retorne el módulo de la verdadera clase de definición. Sin embargo, siempre retornará un módulo con la misma definición, lo que garantiza un diseño de memoria C compatible.

Vida útil del estado del módulo

Cuando un objeto de módulo se recolecta como basura, se libera su estado de módulo. Para cada puntero a (una parte de) el estado del módulo, debe tener una referencia al objeto del módulo.

Por lo general, esto no es un problema, porque los tipos creados con PyType_FromModuleAndSpec() y sus instancias contienen una referencia al módulo. Sin embargo, debe tener cuidado en el recuento de referencias cuando hace referencia al estado del módulo desde otros lugares, como devoluciones de llamada para bibliotecas externas.

Problemas abiertos

Varios problemas relacionados con el estado por módulo y los tipos heap todavía están abiertos.

Las discusiones sobre cómo mejorar la situación se llevan a cabo mejor en el capi-sig mailing list.

Alcance por clase

Actualmente (a partir de Python 3.11) no es posible adjuntar estado a tipos individuales sin depender de los detalles de implementación de CPython (que pueden cambiar en el futuro, tal vez, irónicamente, para permitir una solución adecuada para el alcance por clase).

Conversión sin pérdidas a tipos heap

La API de tipo heap no se diseñó para la conversión «sin pérdidas» de tipos estáticos; es decir, crear un tipo que funcione exactamente como un tipo estático determinado.