3. Définir les types d'extension : divers sujets

Cette section vise à donner un aperçu rapide des différentes méthodes de type que vous pouvez implémenter et de ce qu'elles font.

Voici la définition de PyTypeObject, après avoir enlevé certains champs utilisés uniquement dans debug builds :

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 */

    /* Assigned meaning in release 2.0 */
    /* call function for all accessible objects */
    traverseproc tp_traverse;

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

    /* Assigned meaning in release 2.1 */
    /* 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;
    // Strong reference on a heap type, borrowed reference on a static type
    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;
    vectorcallfunc tp_vectorcall;

    /* bitset of which type-watchers care about this type */
    unsigned char tp_watched;
} PyTypeObject;

Cela fait beaucoup de méthodes. Ne vous inquiétez pas trop cependant : si vous souhaitez définir un type, il y a de fortes chances que vous n'implémentiez qu'une petite partie d'entre elles.

Comme vous vous en doutez probablement maintenant, nous allons passer en revue cela et donner plus d'informations sur les différents gestionnaires. Nous ne suivrons pas l'ordre dans lequel ils sont définis dans la structure, car l'ordre des champs résulte d'un certain historique. Il est souvent plus facile de trouver un exemple qui inclut les champs dont vous avez besoin, puis de modifier les valeurs en fonction de votre nouveau type :

const char *tp_name; /* For printing */

Le nom du type – comme mentionné dans le chapitre précédent, cela apparaîtra à divers endroits, presque entièrement à des fins de diagnostic. Essayez de choisir quelque chose qui sera utile dans une telle situation !

Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */

Ces champs indiquent la quantité de mémoire à allouer à l'exécution lorsque de nouveaux objets de ce type sont créés. Python prend en charge nativement des structures de longueur variable (pensez : chaînes, n-uplets), c'est là que le champ tp_itemsize entre en jeu. Cela sera traité plus tard.

const char *tp_doc;

Ici vous pouvez mettre une chaîne (ou son adresse) que vous voulez renvoyer lorsque le script Python référence obj.__doc__ pour récupérer le docstring.

Nous en arrivons maintenant aux méthodes de type basiques -- celles que la plupart des types d'extension mettront en œuvre.

3.1. Finalisation et libération de mémoire

destructor tp_dealloc;

Cette fonction est appelée lorsque le compteur de références de l'instance de votre type tombe à zéro et que l'interpréteur Python veut récupérer la mémoire afférente. Si votre type a de la mémoire à libérer ou un autre nettoyage à effectuer, vous pouvez le mettre ici. L'objet lui-même doit être libéré ici aussi. Voici un exemple de cette fonction :

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

Si votre type prend en charge le ramasse-miettes, le destructeur doit appeler PyObject_GC_UnTrack() avant d'effacer les champs membres :

static void
newdatatype_dealloc(newdatatypeobject *obj)
{
    PyObject_GC_UnTrack(obj);
    Py_CLEAR(obj->other_obj);
    ...
    Py_TYPE(obj)->tp_free((PyObject *)obj);
}

Une exigence importante de la fonction de libération de la mémoire est de ne pas s'occuper de toutes les exceptions en attente. C'est important car les fonctions de libération de la mémoire sont fréquemment appelées lorsque l'interpréteur remonte la pile d'appels Python ; lorsque la pile est remontée à cause d'une exception (plutôt que de retours normaux), les fonctions de libération peuvent voir qu'une exception a déjà été définie. Toute action effectuée par une fonction de libération de la mémoire pouvant entraîner l'exécution de code Python supplémentaire peut détecter qu'une exception a été définie. Cela peut conduire l’interpréteur à se tromper sur la nature de l'erreur. La bonne façon d'éviter cela est d'enregistrer une exception en attente avant d'effectuer l'action non sécurisée et à la restaurer une fois terminée. Cela peut être fait en utilisant les fonctions PyErr_Fetch() et 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_CallNoArgs(self->my_callback);
        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);
}

Note

des limites existent à ce que vous pouvez faire en toute sécurité dans une fonction de libération de la mémoire. Tout d'abord, si votre type prend en charge le ramasse-miettes (en utilisant tp_traverse et/ou tp_clear), certains membres de l'objet peuvent avoir été effacés ou finalisés avant que tp_dealloc ne soit appelé. Deuxièmement, dans tp_dealloc, votre objet est dans un état instable : son compteur de références est égal à zéro. Tout appel à un objet non trivial ou à une API (comme dans l'exemple ci-dessus) peut finir par appeler tp_dealloc à nouveau, provoquant une double libération et un plantage.

À partir de Python 3.4, il est recommandé de ne pas mettre de code de finalisation complexe dans tp_dealloc, et d'utiliser à la place la nouvelle méthode de type tp_finalize.

Voir aussi

PEP 442 explique le nouveau schéma de finalisation.

3.2. Présentation de l'objet

En Python, il existe deux façons de générer une représentation textuelle d'un objet : la fonction repr() et la fonction str() (la fonction print() appelle simplement str()). Ces gestionnaires sont tous deux facultatifs.

reprfunc tp_repr;
reprfunc tp_str;

Le gestionnaire tp_repr doit renvoyer un objet chaîne contenant une représentation de l'instance pour laquelle il est appelé. Voici un exemple simple :

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

Si aucun gestionnaire tp_repr n'est spécifié, l'interpréteur fournira une représentation qui utilise le type tp_name et une valeur d'identification unique pour l'objet.

Le gestionnaire tp_str est à str() ce que le gestionnaire tp_repr décrit ci-dessus est à repr() ; c'est-à-dire qu'il est appelé lorsque le code Python appelle str() sur une instance de votre objet. Son implémentation est très similaire à la fonction tp_repr, mais la chaîne résultante est destinée à être lue par des utilisateurs. Si tp_str n'est pas spécifié, le gestionnaire tp_repr est utilisé à la place.

Voici un exemple simple :

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

3.3. Gestion des attributs

Pour chaque objet pouvant prendre en charge des attributs, le type correspondant doit fournir les fonctions qui contrôlent la façon dont les attributs sont résolus. Il doit y avoir une fonction qui peut récupérer les attributs (le cas échéant) et une autre pour définir les attributs (si la définition des attributs est autorisée). La suppression d'un attribut est un cas particulier, pour lequel la nouvelle valeur transmise au gestionnaire est NULL.

Python prend en charge deux paires de gestionnaires d'attributs ; un type qui prend en charge les attributs n'a besoin d'implémenter les fonctions que pour une paire. La différence est qu'une paire prend le nom de l'attribut en tant que char*, tandis que l'autre accepte un PyObject*. Chaque type peut utiliser la paire la plus logique pour la commodité de l'implémentation.

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

Si accéder aux attributs d'un objet est toujours une opération simple (ceci sera expliqué brièvement), il existe des implémentations génériques qui peuvent être utilisées pour fournir la version PyObject* des fonctions de gestion des attributs. Le besoin réel de gestionnaires d'attributs spécifiques au type a presque complètement disparu à partir de Python 2.2, bien qu'il existe de nombreux exemples qui n'ont pas été mis à jour pour utiliser certains des nouveaux mécanismes génériques disponibles.

3.3.1. Gestion des attributs génériques

La plupart des types d'extensions n'utilisent que des attributs simples. Alors, qu'est-ce qui rend les attributs simples ? Seules quelques conditions doivent être remplies :

  1. le nom des attributs doit être déjà connu lorsqu'on lance PyType_Ready() ;

  2. aucun traitement spécial n'est nécessaire pour enregistrer qu'un attribut a été recherché ou défini, et aucune action ne doit être entreprise en fonction de la valeur.

Notez que cette liste n'impose aucune restriction sur les valeurs des attributs, le moment où les valeurs sont calculées ou la manière dont les données pertinentes sont stockées.

Lorsque PyType_Ready() est appelé, il utilise trois tableaux référencés par l'objet type pour créer des descripteurs qui sont placés dans le dictionnaire de l'objet type. Chaque descripteur contrôle l'accès à un attribut de l'objet instance. Chacun des tableaux est facultatif ; si les trois sont NULL, les instances du type n'auront que des attributs hérités de leur type de base et doivent laisser les champs tp_getattro et tp_setattro à NULL également, permettant au type de base de gérer les attributs.

Les tableaux sont déclarés sous la forme de trois champs de type objet :

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

Si tp_methods n'est pas NULL, il doit faire référence à un tableau de structures PyMethodDef. Chaque entrée du tableau est une instance de cette structure :

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

One entry should be defined for each method provided by the type; no entries are needed for methods inherited from a base type. One additional entry is needed at the end; it is a sentinel that marks the end of the array. The ml_name field of the sentinel must be NULL.

Le deuxième tableau est utilisé pour définir les attributs qui correspondent directement aux données stockées dans l'instance. Divers types C natifs sont pris en charge et l'accès peut être en lecture seule ou en lecture-écriture. Les structures du tableau sont définies comme suit :

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

For each entry in the table, a descriptor will be constructed and added to the type which will be able to extract a value from the instance structure. The type field should contain a type code like Py_T_INT or Py_T_DOUBLE; the value will be used to determine how to convert Python values to and from C values. The flags field is used to store flags which control how the attribute can be accessed: you can set it to Py_READONLY to prevent Python code from setting it.

An interesting advantage of using the tp_members table to build descriptors that are used at runtime is that any attribute defined this way can have an associated doc string simply by providing the text in the table. An application can use the introspection API to retrieve the descriptor from the class object, and get the doc string using its __doc__ attribute.

As with the tp_methods table, a sentinel entry with a ml_name value of NULL is required.

3.3.2. Gestion des attributs de type spécifiques

Pour plus de simplicité, seule la version char* est montrée ici ; le type du paramètre name est la seule différence entre les variations char* et PyObject* de l'interface. Cet exemple fait effectivement la même chose que l'exemple générique ci-dessus, mais n'utilise pas le support générique ajouté dans Python 2.2. Il explique comment les fonctions de gestionnaire sont appelées, de sorte que si vous avez besoin d'étendre leurs fonctionnalités, vous comprendrez ce qui doit être fait.

The tp_getattr handler is called when the object requires an attribute look-up. It is called in the same situations where the __getattr__() method of a class would be called.

Voici un exemple :

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

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

The tp_setattr handler is called when the __setattr__() or __delattr__() method of a class instance would be called. When an attribute should be deleted, the third parameter will be NULL. Here is an example that simply raises an exception; if this were really all you wanted, the tp_setattr handler should be set to NULL.

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

3.4. Comparaison des objets

richcmpfunc tp_richcompare;

The tp_richcompare handler is called when comparisons are needed. It is analogous to the rich comparison methods, like __lt__(), and also called by PyObject_RichCompare() and PyObject_RichCompareBool().

Cette fonction est appelée avec deux objets Python et l'opérateur comme arguments, où l'opérateur est Py_EQ, Py_NE, Py_LE, Py_GE, Py_LT ou Py_GT. Elle doit comparer les deux objets conformément à l'opérateur spécifié et renvoyer Py_True ou Py_False si la comparaison a réussi, Py_NotImplemented pour indiquer que la comparaison n'est pas implémentée et que la méthode de comparaison de l'autre objet doit être essayée, ou NULL si une exception doit être levée.

Voici un exemple d'implémentation, pour un type de données où l'égalité signifie que la taille d'un pointeur interne est égale :

static PyObject *
newdatatype_richcmp(newdatatypeobject *obj1, newdatatypeobject *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. Gestion de protocoles abstraits

Python prend en charge divers « protocoles » abstraits ; les interfaces spécifiques fournies pour utiliser ces interfaces sont documentées dans Couche d'abstraction des objets.

Un certain nombre de ces interfaces abstraites ont été définies au début du développement de l'implémentation Python. En particulier, les protocoles de nombre, de correspondance et de séquence font partie de Python depuis le début. D'autres protocoles ont été ajoutés au fil du temps. Pour les protocoles qui dépendent de plusieurs routines de gestionnaire de l'implémentation du type, les anciens protocoles ont été définis comme des blocs facultatifs de gestionnaires référencés par l'objet type. Pour les protocoles plus récents, il existe des emplacements supplémentaires dans l'objet de type principal, avec un bit d'indicateur défini pour indiquer que les emplacements sont présents et doivent être vérifiés par l'interpréteur (le bit d'indicateur n'indique pas que les valeurs d'emplacement ne sont pas NULL ; il peut être défini pour indiquer la présence d'un emplacement, mais un emplacement peut toujours être vide). :

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

Si vous souhaitez que votre objet puisse agir comme un nombre, une séquence ou un tableau de correspondances, placez l'adresse d'une structure qui implémente le type C PyNumberMethods, PySequenceMethods ou PyMappingMethods, respectivement. C'est à vous de remplir cette structure avec les valeurs appropriées. Vous pouvez trouver des exemples d'utilisation de chacun d'entre eux dans le répertoire Objects de la distribution source de Python. :

hashfunc tp_hash;

Cette fonction, si vous la fournissez, doit renvoyer un condensat pour une instance de votre type de données. Voici un exemple 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 est un type entier signé avec une largeur variable selon la plate-forme. Renvoyer -1 pour tp_hash indique une erreur, c'est pourquoi vous devez faire attention à ne pas le renvoyer lorsque le calcul du hachage est réussi, comme vu ci-dessus.

ternaryfunc tp_call;

Cette fonction est appelée lorsqu'une instance de votre type de données est « appelée », par exemple, si obj1 est une instance de votre type de données et que le script Python contient obj1('hello'), le gestionnaire tp_call est appelé.

Cette fonction prend trois arguments :

  1. self est l'instance du type de données qui fait l'objet de l'appel. Si l'appel est obj1('hello'), alors self est obj1.

  2. args est un n-uplet contenant les arguments de l'appel. Vous pouvez utiliser PyArg_ParseTuple() pour extraire les arguments.

  3. kwds est le dictionnaire d'arguments nommés qui ont été passés. Si ce n'est pas NULL et que vous gérez les arguments nommés, utilisez PyArg_ParseTupleAndKeywords() pour extraire les arguments. Si vous ne souhaitez pas prendre en charge les arguments nommés et qu'il n'est pas NULL, levez une TypeError avec un message indiquant que les arguments nommés ne sont pas pris en charge.

Ceci est une implémentation tp_call très simple :

static PyObject *
newdatatype_call(newdatatypeobject *obj, 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;

These functions provide support for the iterator protocol. Both handlers take exactly one parameter, the instance for which they are being called, and return a new reference. In the case of an error, they should set an exception and return NULL. tp_iter corresponds to the Python __iter__() method, while tp_iternext corresponds to the Python __next__() method.

Tout objet iterable doit implémenter le gestionnaire tp_iter, qui doit renvoyer un objet de type iterator. Ici, les mêmes directives s'appliquent de la même façon que pour les classes Python :

  • Pour les collections (telles que les listes et les n-uplets) qui peuvent implémenter plusieurs itérateurs indépendants, un nouvel itérateur doit être créé et renvoyé par chaque appel de type tp_iter.

  • Les objets qui ne peuvent être itérés qu'une seule fois (généralement en raison d'effets de bord de l'itération, tels que les objets fichiers) peuvent implémenter tp_iter en renvoyant une nouvelle référence à eux-mêmes – et doivent donc également implémenter le gestionnaire tp_iternext.

Tout objet itérateur doit implémenter à la fois tp_iter et tp_iternext. Le gestionnaire tp_iter d'un itérateur doit renvoyer une nouvelle référence de l'itérateur. Son gestionnaire tp_iternext doit renvoyer une nouvelle référence à l'objet suivant dans l'itération, s'il y en a un. Si l'itération a atteint la fin, tp_iternext peut renvoyer NULL sans définir d'exception, ou il peut définir StopIteration en plus de renvoyer NULL ; éviter de lever une exception peut donner des performances légèrement meilleures. Si une erreur réelle se produit, tp_iternext doit toujours définir une exception et renvoyer NULL.

3.6. Prise en charge de la référence faible

L'un des objectifs de l'implémentation de la référence faible de Python est de permettre à tout type d'objet de participer au mécanisme de référence faible sans avoir à supporter le surcoût de la performance critique des certains objets, tels que les nombres.

Voir aussi

documentation pour le module weakref.

For an object to be weakly referenceable, the extension type must set the Py_TPFLAGS_MANAGED_WEAKREF bit of the tp_flags field. The legacy tp_weaklistoffset field should be left as zero.

Concretely, here is how the statically declared type object would look:

static PyTypeObject TrivialType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    /* ... other members omitted for brevity ... */
    .tp_flags = Py_TPFLAGS_MANAGED_WEAKREF | ...,
};

The only further addition is that tp_dealloc needs to clear any weak references (by calling PyObject_ClearWeakRefs()):

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

3.7. Plus de suggestions

Pour savoir comment mettre en œuvre une méthode spécifique pour votre nouveau type de données, téléchargez le code source CPython. Allez dans le répertoire Objects, puis cherchez dans les fichiers sources C la fonction tp_ plus la fonction que vous voulez (par exemple, tp_richcompare). Vous trouverez des exemples de la fonction que vous voulez implémenter.

Lorsque vous avez besoin de vérifier qu'un objet est une instance concrète du type que vous implémentez, utilisez la fonction PyObject_TypeCheck(). Voici un exemple de son utilisation :

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

Voir aussi

Télécharger les versions sources de CPython.

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

Le projet CPython sur GitHub, où se trouve le code source CPython.

https://github.com/python/cpython