Extensions Python avancées
Programmation d’extensions Python avec l’API C
Section titled “Programmation d’extensions Python avec l’API C”La flexibilité de Python permet d’intégrer du code écrit dans des langages compilés comme C ou C++. Ces bibliothèques compilées, appelables depuis Python, sont appelées « extensions ».
Un module d’extension Python est essentiellement une bibliothèque partagée standard (comme .so sous Unix/Linux ou .dll sous Windows) qui expose une interface compatible avec l’interpréteur Python.
Pourquoi écrire des extensions C ?
Section titled “Pourquoi écrire des extensions C ?”- Performance : Pour les tâches gourmandes en calcul où la vitesse de Python est insuffisante, le code C/C++ peut offrir des accélérations significatives.
- Accès aux bibliothèques C : Pour encapsuler des bibliothèques C/C++ existantes et les rendre disponibles dans Python.
- Accès système de bas niveau : Pour effectuer des opérations non directement exposées par les modules Python standards.
Alternatives modernes :
Section titled “Alternatives modernes :”Bien que l’API C directe offre un contrôle maximal, plusieurs outils modernes simplifient souvent le développement d’extensions :
ctypes: Fait partie de la bibliothèque standard. Permet d’appeler des fonctions dans des bibliothèques partagées directement depuis Python sans écrire de code C, utile pour les interfaces simples.cffi: Bibliothèque tierce. Permet d’appeler du code C depuis Python, souvent plus facile que l’API C brute pour les interfaces complexes.Cython: Un langage qui rend l’écriture d’extensions C pour Python aussi simple que Python lui-même. Il compile du code de type Python (avec des déclarations de type C optionnelles) en extensions C optimisées.Pybind11: Une bibliothèque C++ populaire pour créer des liaisons Python pour du code C++.
Ce guide se concentre sur l’API C directe, la méthode de plus bas niveau.
Prérequis pour l’écriture d’extensions C
Section titled “Prérequis pour l’écriture d’extensions C”- Compilateur C : Vous avez besoin d’un compilateur C compatible avec votre installation Python (par exemple, GCC sous Linux, MSVC sous Windows).
- En-têtes de développement Python : Ces fichiers (
Python.het autres) fournissent les définitions nécessaires pour interagir avec l’interpréteur Python. Installez-les via le gestionnaire de paquets de votre système (par exemple,python3-devsur Debian/Ubuntu,python3-develsur Fedora/CentOS) ou assurez-vous qu’ils sont inclus si vous avez compilé Python à partir des sources. Les installateurs Windows les incluent généralement. - Connaissance des systèmes de build : Familiarité avec la compilation de code C et l’utilisation d’outils de build.
De plus, une bonne compréhension de la programmation C et du modèle objet de Python est supposée.
Structure d’une extension C simple
Section titled “Structure d’une extension C simple”Un module d’extension C de base implique généralement ces parties :
- Inclure
Python.h: Fournit l’accès à l’API C de Python. - Fonctions C : L’implémentation réelle des fonctions que vous souhaitez exposer à Python.
- Table de définition des méthodes : Mappe les noms de fonctions Python à leurs implémentations C.
- Structure de définition du module : Décrit le module lui-même (nom, docstring, méthodes).
- Fonction d’initialisation : Le point d’entrée appelé par Python lors de l’importation du module.
1. Inclure Python.h
Section titled “1. Inclure Python.h”Ce fichier d’en-tête doit être inclus en premier dans votre fichier source C.
#include <Python.h>2. Fonctions C (Implémentation de la logique d’extension)
Section titled “2. Fonctions C (Implémentation de la logique d’extension)”Ces fonctions C seront appelées depuis Python. Elles prennent généralement des arguments PyObject* et retournent un PyObject*. PyObject est la structure C fondamentale représentant tout objet Python.
Signatures courantes :
// Fonction prenant des arguments variables (comme *args)static PyObject * my_c_function(PyObject *self, PyObject *args);
// Fonction prenant des arguments variables et des arguments mot-clé (comme **kwargs)static PyObject * my_c_function_kw(PyObject *self, PyObject *args, PyObject *kwargs);
// Fonction ne prenant aucun argumentstatic PyObject * my_c_function_noargs(PyObject *self);self fait généralement référence à l’objet module pour les fonctions au niveau du module. args est un tuple d’arguments positionnels. kwargs est un dictionnaire d’arguments mot-clé.
Les fonctions doivent retourner un PyObject*. Pour retourner le None de Python, utilisez Py_RETURN_NONE. Si une erreur se produit, retournez NULL après avoir défini une exception Python appropriée.
Par convention, les fonctions C sont déclarées static sauf si elles sont nécessaires en externe. La nomenclature combine souvent le nom du module et le nom de la fonction (par exemple, spam_system).
Exemple de fonction C :
// Implémentation d'une fonction C exemple (détails ultérieurs)static PyObject * spam_system(PyObject *self, PyObject *args) { const char *command; int sts;
// Analyse les arguments du tuple Python 'args' if (!PyArg_ParseTuple(args, "s", &command)) { // PyArg_ParseTuple lève une TypeError en cas d'échec return NULL; // Indique une erreur }
// Appelle la fonction de la bibliothèque C sts = system(command);
// Vérifie les erreurs C et lève une exception Python si nécessaire if (sts < 0) { PyErr_SetString(PyExc_OSError, "system() failed"); return NULL; }
// Construit et retourne un entier Python return PyLong_FromLong(sts);}3. Table de définition des méthodes (PyMethodDef)
Section titled “3. Table de définition des méthodes (PyMethodDef)”Ce tableau mappe les noms de fonctions Python, les pointeurs de fonctions C, les types d’arguments et les docstrings.
Structure :
struct PyMethodDef { const char *ml_name; /* Nom de la fonction Python */ PyCFunction ml_meth; /* Pointeur de fonction C */ int ml_flags; /* Drapeaux d'arguments (METH_VARARGS, METH_KEYWORDS, METH_NOARGS) */ const char *ml_doc; /* Docstring */};Les drapeaux déterminent la signature de fonction C attendue :
METH_VARARGS: La fonction attend(PyObject *self, PyObject *args).METH_KEYWORDS: La fonction attend(PyObject *self, PyObject *args, PyObject *kwargs). Peut être combinée (OR) avecMETH_VARARGS.METH_NOARGS: La fonction attend(PyObject *self). Aucun argument n’est autorisé depuis Python.
Le tableau doit se terminer par une entrée sentinelle {NULL, NULL, 0, NULL}.
Exemple de tableau :
static PyMethodDef SpamMethods[] = { // {Nom Python, Fonction C, Drapeaux, Docstring} {"system", spam_system, METH_VARARGS, "Exécute une commande shell."}, // Ajoutez d'autres fonctions ici... {NULL, NULL, 0, NULL} /* Sentinelle */};4. Structure de définition du module (PyModuleDef)
Section titled “4. Structure de définition du module (PyModuleDef)”Cette structure contient toutes les informations sur le module lui-même.
Structure :
static struct PyModuleDef spammodule = { PyModuleDef_HEAD_INIT, "spam", /* Nom du module */ "Exemple de module qui fournit une fonction.", /* Docstring du module */ -1, /* Taille de l'état par interpréteur, -1 signifie aucun état */ SpamMethods /* Table des méthodes définie ci-dessus */ /* D'autres champs comme m_slots, m_traverse, m_clear, m_free peuvent être NULL */};5. Fonction d’initialisation (PyInit_nom_module)
Section titled “5. Fonction d’initialisation (PyInit_nom_module)”C’est la seule fonction non-static. Python l’appelle lorsque le module est importé pour la première fois. Son nom doit être PyInit_ suivi du nom du module spécifié dans le PyModuleDef.
Elle utilise PyModule_Create() avec la structure de définition du module.
Syntaxe :
PyMODINIT_FUNC // Macro assurant la visibilité et le type de retour correctsPyInit_spam(void) { return PyModule_Create(&spammodule);}Exemple simple complet (spam.c)
Section titled “Exemple simple complet (spam.c)”#define PY_SSIZE_T_CLEAN // Recommandé pour l'utilisation moderne de l'API C#include <Python.h>#include <stdlib.h> // Pour system()
// 1. Implémentation de la fonction Cstatic PyObject * spam_system(PyObject *self, PyObject *args) { const char *command; int sts;
if (!PyArg_ParseTuple(args, "s", &command)) { return NULL; } sts = system(command); if (sts < 0) { PyErr_SetString(PyExc_OSError, "system() failed"); return NULL; } return PyLong_FromLong(sts);}
// 2. Table de définition des méthodesstatic PyMethodDef SpamMethods[] = { {"system", spam_system, METH_VARARGS, "Exécute une commande shell."}, {NULL, NULL, 0, NULL} /* Sentinelle */};
// 3. Structure de définition du modulestatic struct PyModuleDef spammodule = { PyModuleDef_HEAD_INIT, "spam", /* nom du module */ "Documentation de module exemple.", /* documentation du module, peut être NULL */ -1, /* taille de l'état par interpréteur du module, ou -1 si le module maintient l'état dans des variables globales. */ SpamMethods};
// 4. Fonction d'initialisationPyMODINIT_FUNC PyInit_spam(void) { return PyModule_Create(&spammodule);}Compilation et installation des extensions
Section titled “Compilation et installation des extensions”Les outils de build standards de Python (setuptools, build) sont utilisés pour compiler et installer les extensions. distutils est largement obsolète.
Vous avez généralement besoin d’un fichier setup.py (ou pyproject.toml avec une configuration de backend de build).
Exemple de setup.py (utilisant setuptools) :
Section titled “Exemple de setup.py (utilisant setuptools) :”from setuptools import setup, Extension
spam_module = Extension('spam', # Nom du module Python sources=['spam.c']) # Liste des fichiers source
setup( name='spam', # Nom du paquet version='1.0', # Version description='Module d'extension C exemple', ext_modules=[spam_module] # Liste d'objets Extension)Commandes de build/installation :
Section titled “Commandes de build/installation :”# Méthode moderne recommandée (nécessite le paquet 'build' : pip install build)# Construit le wheel et le sdist dans le répertoire 'dist/'python -m build
# Installe le wheel construit (remplacez par le nom de fichier wheel réel)pip install dist/spam-1.0-cp310-cp310-linux_x86_64.whl
# --- OU --- (Ancienne installation directe setuptools)# Compile et installe directement dans l'environnement Python actuel# Pourrait nécessiter des privilèges root/admin selon l'environnement# python setup.py installImportation et utilisation de l’extension
Section titled “Importation et utilisation de l’extension”Une fois installée, importez-la et utilisez-la comme n’importe quel autre module Python :
#!/usr/bin/env python3
import spamimport os
print("Docstring:", spam.__doc__)
# Exemple : Lister le contenu du répertoire en utilisant l'extensiontry: # Utilisez une commande sûre pour la démonstration command = f"ls -l {os.getcwd()}" # Utilisez une commande appropriée pour votre OS status = spam.system(command) print(f"\nCommande exécutée avec le statut : {status}")except OSError as e: print(f"Erreur lors de l'exécution de la commande : {e}")except Exception as e: print(f"Une erreur inattendue s'est produite : {e}")Passage de paramètres (PyArg_ParseTuple)
Section titled “Passage de paramètres (PyArg_ParseTuple)”La fonction PyArg_ParseTuple() (et la fonction associée PyArg_ParseTupleAndKeywords()) est utilisée dans vos fonctions C pour extraire les arguments passés depuis Python.
Syntaxe :
int PyArg_ParseTuple(PyObject *args, const char *format, ...);args : Le tuple PyObject* contenant les arguments positionnels.
format : Une chaîne de format spécifiant les types d’arguments attendus.
... : Pointeur vers les variables C où les valeurs analysées seront stockées.
Retourne vrai en cas de succès, faux en cas d’échec (et définit une exception Python).
Codes de format courants :
Section titled “Codes de format courants :”| Code | Type | Signification |
|---|---|---|
s | chaîne | Chaîne Python -> const char* (UTF-8 supposé). Utilisez es ou et pour un contrôle explicite de l’encodage. |
z | chaîne ou Aucun | const char* ou NULL. |
i | entier | Entier Python -> int C. |
l | entier | Entier Python -> long C. |
L | entier | Entier Python -> long long C. |
d | flottant | Flottant Python -> double C. |
f | flottant | Flottant Python -> float C. |
O | objet | Objet Python -> PyObject* (référence empruntée). Utilisez O! pour la vérification de type, O& pour un convertisseur personnalisé. |
p | booléen | Booléen Python -> int C (non-zéro pour Vrai). |
(items) | tuple | Nécessite une séquence, analyse les éléments selon le format inclus. |
| | Indique que les arguments suivants sont optionnels. | |
: | Suivi du nom de la fonction pour les messages d’erreur. | |
; | Suivi du message d’erreur complet. |
Exemple d’analyse de plusieurs arguments :
static PyObject * my_func(PyObject *self, PyObject *args) { int i_val; double d_val; const char *s_val; PyObject *o_val;
// Attend un entier, un double, une chaîne et n'importe quel objet if (!PyArg_ParseTuple(args, "idsO", &i_val, &d_val, &s_val, &o_val)) { return NULL; // Erreur : PyArg_ParseTuple définit une exception }
printf("Analysé : int=%d, double=%f, string='%s'\n", i_val, d_val, s_val); // Traite o_val en utilisant PyObject_Print ou d'autres fonctions API
Py_RETURN_NONE; // Retourne None en cas de succès}Retourner des valeurs (Py_BuildValue)
Section titled “Retourner des valeurs (Py_BuildValue)”Pour retourner des valeurs de votre fonction C à Python, construisez un PyObject* en utilisant des fonctions comme PyLong_FromLong, PyFloat_FromDouble, PyUnicode_FromString, ou la polyvalente Py_BuildValue().
Py_BuildValue() utilise une chaîne de format similaire à PyArg_ParseTuple mais prend des valeurs C en entrée.
Syntaxe :
PyObject* Py_BuildValue(const char *format, ...);Retourne une nouvelle référence à l’objet Python créé, ou NULL en cas d’erreur.
Codes de format courants :
Section titled “Codes de format courants :”| Code | Type C | Signification |
|---|---|---|
s | const char* | Chaîne C -> chaîne Python (UTF-8 supposé). |
z | const char* ou NULL | Chaîne C ou NULL -> chaîne Python ou None. |
i | int | int C -> int Python. |
l | long | long C -> int Python. |
L | long long | long long C -> int Python. |
d | double | double C -> float Python. |
f | float | float C -> float Python. |
O | PyObject* | Passe l’objet, incrémente le compteur de références (propriété de l’appelant). |
N | PyObject* | Passe l’objet, vole la référence (l’appelant perd la propriété). |
(items) | valeurs C… | Construit un tuple Python à partir de valeurs C. |
[items] | valeurs C… | Construit une liste Python. |
{items} | clé, valeur, … | Construit un dict Python (clé1, val1, clé2, val2…). |
Exemple de retour de valeurs :
static PyObject * foo_add_subtract(PyObject *self, PyObject *args) { int a, b; if (!PyArg_ParseTuple(args, "ii", &a, &b)) { return NULL; } // Retourne un tuple (a + b, a - b) return Py_BuildValue("(ii)", a + b, a - b);}Comptage de références
Section titled “Comptage de références”Python utilise le comptage de références pour la gestion de la mémoire. Dans les extensions C, vous devez gérer correctement les compteurs de références en utilisant Py_INCREF(obj) pour incrémenter et Py_DECREF(obj) pour décrémenter le compteur. Ne pas le faire entraîne des fuites de mémoire ou des crashs.
Les fonctions comme PyLong_FromLong et Py_BuildValue retournent de nouvelles références (l’appelant en est propriétaire). PyArg_ParseTuple avec le code de format O retourne une référence empruntée (ne décrémentez pas à moins de l’incrémenter d’abord). Lors du retour d’un objet, la propriété est généralement passée à l’appelant.
Gérer les références avec soin est l’un des aspects les plus complexes de l’écriture d’extensions C. Des outils comme Cython gèrent cela automatiquement.