Bienvenue sur IndexError.

Ici vous pouvez poser des questions sur Python et le Framework Django.

Mais aussi sur les technos front comme React, Angular, Typescript et Javascript en général.

Consultez la FAQ pour améliorer vos chances d'avoir des réponses à vos questions.

Est-ce possible de créer une fonction 'à la volée' en Python?

+2 votes

Préambule: j'ai appris la programmation en atu-didacte, et je n'ai pas encore beaucoup d'expérience en Python. Mon problème vient donc peut-être simplement d'un manque de vocabulaire précis, ou d'un manque sur des notions qui pourraient paraître évidentes à des programmeurs ayant un bon bagage théorique. D'où une question subsidiaire: quels sont les bons 'keywords' pour trouver de l'information pertinente sur le sujet?

Précisions sur mon problème: Je souhaite créer dynamiquement une fonction contenant une série d'équations différentielles à résoudre avec une libraire de Scipy (mais peu importe, la question reste valable dans un autre contexte). Après des recherches sur le web, j'ai cru comprendre que c'était une forme de "metaprogramming", et que dans ce domaine, Python n'est pas le mieux placé (par rapport aux langages dérivés de Lisp par exemple).
La fonction que je veux obtenir serait obtenue à partir de bouts de code (contenant des équations "atomiques") combinés entre eux selon des réglages définis par l'utilisateur, afin d'obtenir un modèle plus complexe décrivant le système à étudier (pour ceux que ça intéresse, en pharmacologie). Il est impératif que le résultat de ce "collage" soit une fonction car tous les outils que j'ai trouvés pour résoudre les équations n'acceptent que des fonctions en entrée (pas d'objets autres donc).
Les macros Lisp semblent répondre à cette problématique, mais pas d'équivalent en Python si j'ai bien compris (et j'ai pas envie de me lancer dans Lisp alors que je ne maîtrise pas encore Python). Il semblerait qu'il y ait des tentatives de librairies pour pallier ça mais j'ai du mal à voir si c'est adapté ou non.
Donc mon premier réflexe: écrire du code pour génerer un fichier qui écrirait la fonction, puis lire le fichier pour charger la fonction via exec (si j'ai bien compris). Mais plein de gens semblent déconseiller cette approche.
Auriez vous d'autres pistes à me proposer svp? Une idée générale avec les bons mots clefs suffit, j'irai chercher par moi-même.

demandé 17-Sep par sifratus (150 points)

Je ne suis pas sur d'avoir bien compris la question, mais tu devrais peut etre regarder ce qui se fait du coté de sympy.

@yoch Merci pour la suggestion, j'avais déjà jeté un oeil sur cette lib, mais sympy gère les modèles de manière symbolique (je ne suis pas matheux, mais en gros, ça manipule les équations pour trouver des solutions exactes, comme on l'apprend à l'école; ex: trouver les racines d'un polynome), or les modèles qui m'intéressent, basés sur des équations différentielles ne peuvent généralement pas être résolus ainsi (sauf cas très simples), et nécessitent une résolution numérique (qui est une approximation de la solution). Scipy fournit des outils pour ça.

Merci pour la page, je vais la lire en détail.
De ce que j'ai pu voir en la survolant, simpy fait appel à la librairie que j'utilise (voir par exemple la fenêtre de code 8: from scipy.integrate import odeint), donc ne traite pas les ODE par elle-même.

L'exemple pourrait toutefois m'être utile car la fenêtre de code 9 montre qu'odeint peut aussi prendre comme argument une fonction qui retourne une liste d'expressions représentant les équations mathématiques. Encore un truc différent de l'array avec les appels de fonctions que j'ai posté par ailleurs comme réponse potentielle.
Décidemment, la doc de scipy.integrate n'est pas très prolixe, ou alors je suis encore plus mauvais en Python que je ne le pensais :/

Et la partie finale pourrait m'être utile pour l'optimisation de la vitesse de résolution, simpy permettant de calculer automatiquement la matrice de dérivées partielles qui peut être passé en argument optionnel d'odeint. Mais ça, ça sera quand j'aurais réussi à faire quelque chose qui marche :)

En tout cas, merci pour vos conseils. Si j'arrive à traduire le code Matlab de manière correcte (opération en cours, ça avance), je posterai un lien ici vers le dépôt github pour ceux que ça peut intéresser.

3 Réponses

+2 votes

Comme je comprends ton problème, je dirais que l'option "metaprogramming" est excessive.

Il est difficile de t'aider sans avoir plus de concret.
Tu veux "coller" des "équations atomiques". Si on pouvait voir un exemple simple de ces "équations atomiques" et de la fonction finale (même si elle est écrite en dur), alors peut-être que quelqu'un aura des propositions pour faire ce collage "à la volé".

Ce qui m'échappe en particulier dans ton problème, c'est comment ton utilisateur va définir son "collage" ? via du code python qu'il aura écrit ? via une interface graphique ? autre ?
Dans le cas où l'utilisateur écrit du code, on peut toujours imaginer laisser l'utilisateur écrire comme un grand sa propre fonction.

Une remarque en passant : il est très simple de faire passer un objet pour une fonction.

répondu 17-Sep par bubulle (2,212 points)

Merci pour ta réponse. J'avais déjà vu un commentaire sur StackOverflow qui indiquait comme toi qu'il est facile de faire passer un objet pour une fonction mais sans plus de précision. Je n'ai pas trouvé d'articles sur le sujet (encore un problème de mots clés incorrects à mon avis), je suis preneur :)

Pour préciser un peu plus ma requête, pour les plus courageux d'entre vous): ce que cherche à faire est créer un petit programme pour faire des modèles dits PBPK (pour ceux que ca intéresse, c'est ça: https://en.wikipedia.org/wiki/Physiologically_based_pharmacokinetic_modelling)
Je me lance sur ce sujet car après avoir suivi différentes formations en Python, il faut bien que je me lance sur un vrai projet pour réellement progresser. Or j'ai une expérience professionnelle antérieure sur le sujet, je maîtrise donc la théorie, ce qui me permettra de me concentrer sur la partie programmation. En revanche, je n'ai rien trouvé de significatif en Python pour me servir de rampe de lancement; j'ai trouvé des choses sous R et surtout un programme Matlab que je suis en train de traduire en Python (mais qui utilise des modèles beaucoup plus simples que ceux que j'aimerais avoir au final).

Pour illustrer un peu, voir un exemple d'un modèle PBPK codé en R (assez volumineux car suit cinq molécules différentes en même temps et étudie leurs interactions); voir le fichier attaché au post du lien.
Cet exemple est très illustratif du fait que les modèles PBPK ont des sortes de motifs récurrents: les équations dans la plupart des organes (=des compartiments) sont les mêmes, à part le nom de variable. Ce qui change, ce sont les valeurs numériques des paramètres des équations.
Ex extrait de ce modèle (le nom des variables est particulièrement court dans ce modèle, c'est pas très lisible):

#Xylene dans le compartiment FAT (aka le gras). 
RXF = QF*(CXA-CXVF) #rate of acumulation of X in FAT
dXF = RXF 
CXF=XF/VF #concentation of X in FAT
CXVF=CXF/PF_X

#Xylene dans le compartiment RPT (Tissus richement vascularisés)
RXR = QR*(CXA-CXVR) #rate of acumulation of X in RPT
dXR = RXR 
CXR=XR/VR #concentation of X in RPT
CXVR=CXR/PR_X

On remplace le F (de FAT) par R (pour RPT) et on tombe sur la même chose.
Que ce soit pour R ou Matlab ou Python, le principe de résolution de ce type de modèle est le même : on met toutes les équations sous une forme particulière dans une fonction qui sera évaluée par un outil spécialisé (par exemple scipy.integrate.odeint en Python)

Donc option 1, je code le modèle comme dans l'exemple R, traduit en Python, et je le fait tourner. Ca marche, je l'ai fait. Mais c'est très rigide car c'est un modèle donné, or il existe des variations sur le jeu d'équations utilisables.
Donc option 2, faire une bibliothèque figée de modèle. Mais j'ai donné par le passé, c'est pénible à maintenir/développer.
D'où ma recherche d'une technique pour créer cette fonction contenant les équations "à la volée" (peut-être pas le bon terme), en se basant sur une librairie de bouts de code (snippets?) validés qui seraient combinés automatiquement selon les choix de l'utilisateur, entrés via un fichier de config par exemple, ou ultérieurement un GUI (ex: je veux un modèle réduit à 4 tissus avec élimination dans le foie selon l'équation X, ou bien un modèle avec 13 compartiments pour avoir plus de détails, etc.)

Il se trouve que le logiciel que j'utilisais dans un ancien poste (un concurrent de Matlab, aujourd'hui disparu, paix à son âme), avait un système dit de "macros" qui m'avait permis d'implémenter cette approche. J'avais défini toute une série de macros, que je combinais rapidement et facilement pour créer des modèles complexes
En simplifiant, appliqué au code cité ci-dessus, ça donnait:

Macro Tissu(Tissu, Molecule)
    R*Molecule**Tissu* = Q*Tissu**(C*Molecule*A-C*Molecule*V*Tissu*) #rate of acumulation of X in *Tissu*
    d*Molecule**Tissu* = R*Molecule**Tissu* 
    C*Molecule**Tissu*=*Molecule**Tissu*/V*Tissu* #concentation of *Molecule* in *Tissu*
    C*Molecule*V*Tissu*=CX*Tissu*/P*Tissu*_*Molecule*
End Macro

J'aurais utilisé par exemple

Tissu("F","X")

et

Tissu("R","X")

pour recréer les équations mentionnées plus haut. 3 lignes de plus pour coder les 3 autres tissus. Le modèle R de plus de 500 lignes en ferait beaucoup moins avec ce système, et serait facile à modifier.

La macro remplaçait Molecule et Tissu dans le code du modèle (qui était ensuite traduit et compilé en C dans ce software)
J'ai trouvé un article montrant que Lisp pouvait faire ce type d'opération très facilement. Si j'ai bien compris, ça n'existe pas en Python, ou peut-être via des librairies, mais plus compliqué.

Après ces explications, je me demandais si quelqu'un aurait une piste pour obtenir ce genre de choses en Python.
J'envisage d'y aller un peu bourrin faute de solution plus élégantes: je crée un fichier, avec une première ligne "def mafonctionpleinedequations(*arg):", puis je le remplis bout par bout en écrivant les équations qui seraient stockées dans un objet Python (gros dictionnaire, ou un truc plus évolué), selon les réglages choisis. Puis je l'execute/eval pour charger la fonction en mémoire.
Il existe j'imagine une façon de faire la même chose directement en mémoire plutôt que de passer par un fichier (mais je suis trop débutant pour me lancer directement là-dedans).
Et je suis persuadé que des types beaucoup plus intelligents que moi se sont déjà heurtés à un problème similaire en Python et l'ont résolu de manière beaucoup plus élégante. Mais je n'ai pas trouvé.

Les closures me permettraient peut-être de réutiliser un bloc de code en redéfinissant les paramètres numériques, mais je ne vois pas comment coller les fonctions ainsi redéfinies entre elles. J'ai pas l'impression que c'est une bonne piste. J'ai commencé à regarder les créations "dynamiques" d'objets (via type) mais je suis assez vite perdu...

Désolé pour ces longues explications, j'avais essayé d'éviter les détails dans le post d'origine, mais effectivement, dur d'être clair en étant concis (et pas sûr d'avoir réussi à éclaircir avec beaucoup plus de mots).

Faire passer un objet pour une fonction?

J'ai regardé ton lien sur les modèles PBPK.
Je comprends mieux ton soucis d'éviter la duplication de code, l'exemple est R est frappant.

Si je lis bien, on y trouve des compartiments qui ont chacun un état propre et une équation d'évolution qui dépend aussi de l'état des compartiments connectés.

A ce stade, produire la fonction à passer au solver de scipy n'est pas le plus gros problème, il faut d'abord trouver le moyen de décrire dans le code la structure du modèle PBPK :

  • les compartiments et leurs caractéristiques propres (Vi, Pi, ...), qui peuvent être de plusieurs types ;
  • les connexions entre les compartiments, qui peuvent être de plusieurs types.

A partir de là, on peut tout à fait écrire une fonction générique qui prend en entrée la structure du modèle (compartiments + connexions) et un état des compartiments (les Qi) et calcule le terme source de chaque équation différentielle.

C'est en tout cas la première piste que j'explorerais.

Merci d'avoir pris le temps de consulter les liens pour tenter de m'aider. Créer 'manuellement' le code décrivant la structure du modèle (les équations) n'est pas vraiment le plus gros problème pour moi (même s'il est probable que mon manque d'expérience en Python conduise à quelque chose de peu optimisé et donc de lent). J'ai réussi à faire tourner une version simplifié du modèle R sous Python (en ne considérant qu'une molécule sur les 5). Faire grossir le modèle pour avoir les 5 ne devrait pas présenter de grosse difficulté technique si le module de scipy arrive à gérer de si gros modèle dans un délai raisonnable sur un PC de base).
Sur la base de mon expérience passée sur le logiciel propriétaire que je mentionnais dans ma question, j'essaye toutefois d'anticiper le fait d'avoir un code suffisamment modulaire pour éviter de devoir écrire "en dur" une nouvelle fonction (assez grosse pour un modèle PBPK) pour chaque variante de modèle. Mais la modularité que je recherche implique :
1) de pouvoir générer une fonction programmatiquement
2) idéalement à partir de bouts de code "atomiques" (dans le sens qu'ils ne peuvent pas être encore plus décomposés) qui seraient assemblés "dynamiquement" / "à la volée" (je ne sais pas quel est le bon terme à employer) lors de l'emploi du script que j'envisage.
J'ai trouvé une piste (voir ma réponse) qui semble intéressante, même si je ne comprends pas bien pourquoi ça marche.

+1 vote

J'ai peut-être trouvé une réponse (au moins partielle) à ma question première dans ce projet Github https://github.com/alexrd/pk

Dans le fichier https://github.com/alexrd/pk/blob/master/run_pk.py, l'auteur décompose son modèle en plusieurs fonctions

def I_of_t (t):
    isum = 0
    for dt, conc in zip(dose_time, dose_conc):
        isum += step(t-dt)*conc*np.exp(-R[0]*(t-dt))
    return isum

def step(x):
    return 1 * (x > 0)

def dIblood_dt (X,t):
    dIb_dt = R[0]*I_of_t(t) - X[0]*(R[1] + R[2]) + X[1]*R[3]
    return dIb_dt

def dItissue_dt (X,t):
    dIt_dt = R[2]*X[0] - X[1]*(R[3] + (1.-X[2])*Etot*R[4]) + X[2]*R[5]*Etot
    return dIt_dt

def df_dt (X,t):
    df_dt = X[1]*R[4] - X[2]*(X[1]*R[4] + R[5])
    return df_dt

def dX_dt(X,t):
    return np.array([dIblood_dt(X,t), dItissue_dt(X,t), df_dt(X,t)])

Il "regroupe" ces équations en une array dans la fonction finale dXdt puis il utilise cette fonction dXdt comme argument dans odeint.

X, infodict = integrate.odeint(dX_dt,X0,t,full_output=True)

Ca fonctionne, j'ai pu le tester. Le modèle sous-jacent semble très simple, mais j'avoue que je ne comprends pas encore bien compris le mécanisme utilisé.
De ce que je comprends, l'array ne renvoie pas une liste de références aux autres fonctions, mais une liste d'appels à ces fonctions (puisqu'elles sont suivies de (X,t)). Or je ne vois pas bien comment odeint peut prendre ça en charge.
Mais bon, je pense pouvoir utiliser ça pour générer via du code une array "à la volée" : ce n'est peut-être pas le terme le plus approprié, ce que je veux dire ici c'est que je devrais pouvoir moduler le contenu de l'array (et donc la structure du modèle utilisé) en fonction de réglages de l'utilisateur.
Je vais creuser ça. Merci à ceux qui ont pris le temps de me répondre.
Pour ma question secondaire (mécanisme de type macro pour pouvoir générer plus facilement du code pour modèle PBPK), j'y reviendrai plus tard, c'est moins crucial (plus une question de confort de programmation)

Je suis toutefois toujours intéressé pour avoir des détails sur la remarque "il est très simple de faire passer un objet pour une fonction.".
Est-ce bien en utilisant la fonction magique _ _ call _ _ qu'on peut faire ça?

répondu 27-Sep par sifratus (150 points)
+1 vote

Comment faire une classe dont les instances se comportent comme fonction ?

Voici un exemple simple de classe qui porte une donnée, propre à chaque instance, (l'attribut y) et qui se comporte comme une fonction :

class A:
    def __init__(self, y):
        self.y = y
    def __call__(self, x):
        return x + self.y

A l'usage, ça donne :

a = A(12)
res = a(30)
print(res)  # affiche 42

On voit que l'objet a (de classe A) se comporte comme une fonction (ex a(30)).

répondu 27-Sep par bubulle (2,212 points)
...