logo le blog invivoo blanc

Programmation Dynamique

7 avril 2022 | Python | 0 comments

Python est un langage fortement dynamique… Mais, dans les faits, qu’est-ce que cela signifie ? Qu’est-ce que cela peut nous apporter ? Grâce à ce dynamisme, Python permet de résoudre des problèmes de manière élégante et compacte là où les langages classiques nécessiterait beaucoup de codes.

Définition de programmation dynamique

Wikipédia

Si on se réfère à Wikipédia, la programmation dynamique est définie par :

On utilise le terme langage de programmation dynamique en informatique pour décrire une classe de langage de haut niveau qui exécutent au moment de l’exécution des actions que d’autres langages ne peuvent exécuter que durant la compilation. Ces actions peuvent inclure des extensions du programme, en ajoutant du code nouveau, en étendant des structures de données et en modifiant le système de types, cela pendant l’exécution du programme. Ces comportements peuvent être émulés dans pratiquement tous les langages de complexité suffisante, mais les langages dynamiques ne comportent pas de barrière, tel que le typage statique, empêchant d’obtenir directement ces comportements.

Langage dynamique

L’un des premiers langages dynamiques fut Lisp. Basé sur la théorie du l-calcul, ce langage a été un des outils prépondérants sur l’amélioration des techniques et langages de programmation : le premier langage objet qui a eu du succès « Smalltalk » a été écrit en Lisp. L’une des particularités du Lisp est qu’un programme est une donnée et qu’une donnée peut devenir un programme : c’est une des raisons pour laquelle il a été à la base des recherches en intelligence artificielle.

Nombre de langages sont aujourd’hui dynamiques à des degrés divers ; voici certains parmi les plus connus et les plus utilisés :

  • Javascript : pour le web dynamique
  • C# (en version 4.0 ou supérieure)
  • Erlang
  • Python : qui est utilisé aussi bien dans les classes de seconde que dans les entreprises de pointe comme Google ou Dropbox
  • Lua : utilisé dans les moteurs de jeux vidéo

Si le Lisp reste un langage que j’apprécie notamment grâce à sa forme préfixée pour le calcul formel ; Python l’a remplacé, dans les faits, pour les tâches quotidiennes nécessitant des solutions rapides à mettre en œuvre.

Les variables en programmation dynamique

Les variables sont des éléments essentiels d’un programme : elles servent à stocker les états… La notion de « dynamisme » pour les variables peut prendre différentes formes :

  • Typage dynamique
  • Création de variables à la volée

Typage dynamique vs typage statique

Les détracteurs de Python avancent souvent le fait que le typage statique est plus sûr : les problèmes de type sont détectés à la compilation plutôt qu’à l’exécution.

En effet dans les langages statiques (Java, C ou C++ par exemple), les variables sont déclarées et typées avant d’être utilisées permettant au compilateur de faire nombres de tests avant de générer le binaire. Par exemple si on prend le petit programme suivant :

double linear_interpolation( double a, double b, double x )
{
    double v = ( 1 - x ) * a + x * b;
    return v;
}


int main()
{
    // appel correct qui compilera
    double r1 = linear_interpolation( 10, 20, 0.5 ); // appel correct

    // ne compilera pas
    std::string s1( "toto" ), s2( "tata" );
    double r2 = linear_interpolation( s1, s2, 2 ); // erreur de type
    return 0;
}

il génèrera comme erreur de compilation :

C:\personnel\private\tests\essai\main.cpp:19:39: error: cannot convert 'std::__cxx11::string' {aka 'std::__cxx11::basic_string<char>'} to 'double'
     double r2 = linear_interpolation( s1, s2, 2 ); // erreur de type
                                       ^~

En Python, par défaut, le type des variables n’est définit qu’au moment de l’affectation. Et de ce fait les problèmes ne sont vus qu’à l’exécution… En reprenant l’exemple développé en C++ et en le portant en Python on obtient :

def linear_interpolation( a, b, x ):
    r = ( 1 - x ) * a + x * b
    return r

r1 = linear_interpolation( 10, 20, 0.5 )
print( type(r1), r1, sep=' : ' )
r2 = linear_interpolation( "toto", "tata", 2 )
print( type(r2), r2, sep=' : ' )

Le code s’exécute sans erreur et affiche les valeurs suivantes :

  • <class ‘float’> : 15.0
  • <class ‘str’> : tatatata

Car en Python on peut multiplier un entier par une chaîne de caractères… Le code s’exécute mais, dans le cas présent, le résultat n’est pas celui attendu !

L’autre avantage du typage statique est la performance : les variables sont associées à un espace mémoire par une adresse et une taille dépendant du type. L’accès à la donnée est donc direct. Dans le cadre du typage dynamique on doit passer par un « dictionnaire » qui lie le nom de la variable à un espace mémoire alloué (dynamiquement) : l’accès est donc le coût d’une recherche dans cette structure de donnée.

Création dynamique de variables

Python ne dispose pas que d’un typage dynamique, il permet aussi de créer des variables à la volée. En effet, nous avons un accès direct aux dictionnaires des variables globales ou locales :

  • globals() : retourne le dictionnaire des variables globales
  • locals() : retourne le dictionnaire des variables locales

Avec le code suivant :

names  = "xyz"
values = [ 1, "toto", [ 1, 2, 3 ] ]
d      = globals()

for name, value in zip( names, values ):
    d[ name ] = value

on obtient la liste des variables suivante dans Spyder :

Ajout de variables à un objet

En python, tout est objet. Et les objets peuvent être étendu par ajout d’attributs. Nous disposons des fonctions suivantes pour manipuler la structure interne des objets Python:

  • def hasattr( obj, attr_name )
    Le résultat est True si la chaîne attr_name est le nom d’un des attributs de l’objet, sinon False. L’implémentation appelle getattr(object, name) et regarde si une exception AttributeError a été levée.

  • def getattr( obj, attr_name [, default ]  )
    Retourne la valeur de l’attribut associé au nom dans attr_name de l’objet obj. attr_name doit être une chaîne de caractères. Si la chaîne est le nom d’un des attributs de l’objet, le résultat est la valeur de cet attribut. Par exemple, getattr(x, ‘foobar’) est équivalent à x.foobar. Si l’attribut n’existe pas, et que default est fourni, il est renvoyé, sinon l’exception AttributeError est levée.

  • def setattr( obj, attr_name, attr_value )
    C’est la fonction complémentaire de getattr. Les arguments sont : un objet Python, une chaîne de caractères, et une valeur à associer à l’attribut. La chaîne peut nommer un attribut existant ou un nouvel attribut. La fonction assigne la valeur à l’attribut, si l’objet l’autorise. Par exemple, setattr(x, ‘foobar’, 123) équivaut à x.foobar = 123.

Cela peut sembler peu de chose mais c’est extrêmement utile : le module argparse en fait usage pour retourner args :

import argparse

parser = argparse.ArgumentParser( description = 'Process some integers.' )
parser.add_argument( 'integers',
                     metavar = 'N',
                     type    = int,
                     nargs   = '+',
                     help    = 'an integer for the accumulator' )
parser.add_argument( '--sum',
                     dest    = 'accumulate',
                     action  = 'store_const',
                     const   = sum,
                     default = max,
                     help    = 'sum the integers (default: find the max)' )

args = parser.parse_args()

‘args’ est initialisé en utilisant setattr à l’intérieur de la fonction parse_args. Cette façon de programmer permet de créer à la volée des variables.

Ce mécanisme permet de mettre en place un système qui chargerait une configuration à partir d’un fichier et créerait des variables. Nous avons une application qui s’exécute sur plusieurs environnements (production, intégration, développement). Pour chaque environnement nous avons un fichier « config.txt » contenant les paramètres utiles (connexion au serveur, à la base de données, les répertoires utiles, etc…) :

# connection
connection.server.hostname=192.77.32.1
connection.server.port=8080
connection.server.account.user=zeus
connection.server.account.password=jupiter
connection.db.name=MY_DB
connection.db.account.user=aphrodite
connection.db.account.password=venus

# directories
directory.log=C\Temp\log
directory.input=C:\Temp\input
directory.output=C:\Temp\output 

Avec le programme suivant, nous allons pouvoir lire ce fichier et nous servir des valeurs lues :

def add_variable( name, value ):
    class A:
        pass

    def _add( obj, names, value ):
        n, *others = names
        if 0 == len( others ):
            setattr( obj, n, value )
        else:
            try:
                v = getattr( obj, n )
            except:
                v = A()
                setattr( obj, n, v )
            _add( v, others, value )

    variables  = globals()
    n, *others = name.split( '.' )
    if 0 == len( others ):
        variables[ n ] = value
    elif n in variables:
        _add( variables[ n ], others, value )
    else:
        v              = A()
        variables[ n ] = v
        _add( v, others, value )


def load_file( filename ):
    with open( filename, "r" ) as file:
        for line in file:
            line = line.strip()
            if 0 == len( line ) or line[ 0 ] == '#':
                continue
            
            name, value = [ x.strip() for x in line.split( '=' ) ]
            add_variable( name, value )


load_file( "C:\\Temp\\config.txt" )
print( connection.server.hostname )
print( connection.server.port )
print( connection.server.account.user )
print( connection.server.account.password )
print( connection.db.name )
print( connection.db.account.user )
print( connection.db.account.password )
print( directory.log )
print( directory.input )
print( directory.output )

La fonction « load_file » lit le fichier ligne par ligne et la fonction « add_variable » crée les variables et les attributs à la volée.

Evaluation programmation dynamique

Nous avons appris à créer des variables et des attributs dynamiquement. Mais est-on capable d’évaluer des formules ou du code dynamiquement ? Si j’ai une formule récupérer dans un formulaire, puis-je en évaluer la valeur en fonction ?

Evaluation de formule

C’est en 2000 que j’ai eu la première fois besoin d’évaluer dynamiquement des formules. Je travaillais sur un logiciel de CAO et je créais un plug-in en C++ qui allait permettre de créer des objets 3D à partir d’équations paramétriques saisies par un utilisateur dans une fenêtre de l’application. Créer une telle fonctionnalité m’avait demandé beaucoup de codes mais en Python cela s’avère beaucoup plus facile ; en effet, il existe une fonction « eval » qui facilite la tâche :

from math import *

formula = input( "formule?" )
a       = float( input( "a?" ) )
b       = float( input( "b?" ) )
nb      = int( input( "nb?" ) )

h = ( b - a ) / nb
for i in range( nb + 1 ):
    x = a + i * h
    print( "f(", x, ") =", eval( formula ) )

En l’exécutant on obtient :

formule?x*x
a?0
b?1
nb?10
f( 0.0 ) = 0.0
f( 0.1 ) = 0.010000000000000002
f( 0.2 ) = 0.04000000000000001
f( 0.30000000000000004 ) = 0.09000000000000002
f( 0.4 ) = 0.16000000000000003
f( 0.5 ) = 0.25
f( 0.6000000000000001 ) = 0.3600000000000001
f( 0.7000000000000001 ) = 0.4900000000000001
f( 0.8 ) = 0.6400000000000001
f( 0.9 ) = 0.81
f( 1.0 ) = 1.0

Les fonctions paramétriques

En Python, une fonction est un objet comme un autre : nous pouvons créer des alias à des fonctions et nous pouvons retourner une fonction d’une fonction. Cette fonctionnalité peut sembler obscure, au premier abord, et peu utile mais c’est, en fait, une fonctionnalité extrêmement utile de Python : elle est notamment à la base des décorateurs.

Prenons l’exemple suivant :

def f( a ):
    def _f( x ):
        return x + a
    return _f

h = f(3)
print( h, end='\n\n' )

for i in range(5):
    print( h(i) )

Lorsque l’on fait « h=f(3) », h est une fonction : <function f.<locals>._f at 0x0000023C1B386DC0>. Et dans la boucle, le programme affiche bien la valeur i+3 à chaque itération. « f » est une fonction qui crée une nouvelle fonction (que l’on stocke dans « h ») qui dépend des paramètres de « f ». Certes c’est moins dynamique que la fonction « eval » mais cela permet d’adapter du code en fonction de conditions.

Cela permet de construire dynamiquement des fonctions spécialisées ou des fonctions permettant de modifier des fonctions existantes via le mécanisme des décorateurs. Supposons que nous souhaitions rajouter une fonctionnalité de logging :

def log( func ):
    def wrapper( *args, **kwargs ):
        print( F"enter in {func.__name__}" )
        res = func( *args, **kwargs )
        print( F"exit from {func.__name__}" )
        return res

    return wrapper

@log
def compute( x ):
    return x*x*x

print( compute(3) )

Dans ce cas-là, « wrapper » dépend de la fonction passé en paramètre de la fonction « log » et la fonction paramétrique est créée en appliquant « @log » à la fonction « compute ».

Création de module

Un module peut être un simple fichier Python. Et la commande « import » permet de charger un module. La question que l’on peut se poser est : peut-on créer dynamiquement un module et le charger ?

A quoi pourrait servir ce type de fonctionnalité me direz-vous ? Plaçons-nous dans un contexte embarqué avec des ressources limitées comme un petit robot piloté par un programme Python. Notre seul moyen de communication avec notre robot est via un réseau non-filaire (un wifi par exemple). La tâche que doit accomplir notre robot est programmé dans un module appelé « mission ». Sa tâche terminée le robot a encore de l’autonomie. Il nous faut donc mettre à jour sa mission :

  • envoyer un fichier texte contenant la nouvelle mission
  • recharger le module via un appel à importlib.reload
  • exécuter la fonction principale du module « mission »

Essayons le code suivant :

import importlib

while True:
    formula = input( "f(x)=" )
    if len(formula) == 0:
        break
    
    texte = F"""
from math import *

def f(x):
    return {formula}
    """
    
    with open( "my_module.py", "w" ) as file:
        file.write( texte )
    
    try:
        importlib.reload( my_module )
    except:
        import my_module
    print( my_module.f(3) )

Si je l’exécute et, qu’à la question « f(x)= » je rentre « x*x » comme formule, j’obtiens l’affichage de 9 ce qui est la bonne réponse. Si, à l’itération suivante, je rentre « x*sin(x) » comme formule j’obtiens 0.4233600241796016 comme résultat (ce qui est bien 3*sin(3)).

Compilation : convertir un texte en code exécutable

Python fournit différentes fonctions permettant d’accéder directement à l’exécution ou à la compilation (en byte-code) d’un répertoire, d’un fichier ou d’une chaîne de caractères. Nous allons nous concentrer uniquement sur la fonction « exec ».

from math import *

fct  = input( "g(x)? " )
code = F"""
def g(x):
    return {fct}
"""

exec( code )
for i in range( 10 ):
    print( "g(", i, ")=", g( i ) )

En tapant « 5*x*x » à la question « g(x)? », nous créons dynamiquement la fonction g grâce au code contenu par la chaîne de caractères « code », puis nous évaluons la fonction g avec des arguments comme si c’était une fonction Python normale. La fonction « exec » se base sur le contenu de « globals() » et « locals() ».

Il existe une fonction « compile » qui permet de compiler du code avec des options d’optimisation. Le résultat étant un objet « code » qui peut ensuite être exécuté via « exec »…

Sérialisation/Désérialisation

Sauvegarder et restaurer les données est une nécessité pour toute application. Mais cela devient aussi un impératif si l’on doit échanger des données via un réseau. Convertir une donnée en un flux de bytes est appelé marshalling en anglais (sérialisation) et l’action inverse est appelé unmarshalling (désérialisation). Avoir ce type de fonctionnalité disponible participe aux créations dynamiques.

pickle

Python propose le module pickle qui est le plus simple à mettre en œuvre tout en restant efficace pour sauvegarder les combinaisons des types simples suivants :

  • NoneType
  • bool : les booléens à valeur True ou False
  • int : les entiers longs
  • float : nombres flottants en double précision
  • complex : les nombres complexes
  • str : chaînes de caractères
  • list : listes
  • dict : dictionnaires
  • tuple : tuple
  • set : ensemble

Il permet de sauvegarder et/ou restaurer des données.

dill

« dill » est une extension que l’on peut télécharger sur PyPI via la commande : « pip install -U dill ». C’est une version améliorée de pickle car elle permet de sérialiser/désérialiser des types de données supplémentaires :

  • des instances de classes
  • fonctions avec yield
  • fonctions lambda
  • nested functions : fonctions paramétriques
  • un objet « code »

Voici un exemple de sérialisation de fonction :

import dill as pickle
from math import *

def f(x):
    return x*sin(x)
with open( "c:/temp/objet.pickle", "wb" ) as file:
    pickle.dump( f, file )

Et voici le désérialisation associée :

import dill as pickle

with open( "c:/temp/objet.pickle", "rb" ) as file:
    f = pickle.load( file )

for i in range( 11 ):
    x = 0.1 * i
    print( F"f({x}) = {f(x)}" )

Cette méthode est intéressante car elle permet de transmettre du code sous un format binaire entre un client et un serveur. Prenons le cas d’une application client lourd de type CAO (modélisation mécanique 3D) qui contient des fonctionnalités gratuites et d’autres payantes. Les fonctionnalités sont trop lourdes en données à transférer ou en calculs pour les exécuter du côté serveur : on souhaite les exécuter côté client. Afin de réduire le risque de piratage des fonctions payantes, nous pouvons mettre en place la mécanique suivante :

  • clic sur la fonctionnalité payante
  • connexion au serveur
  • vérification de la licence ou du paiement
  • téléchargement de la fonctionnalité via un fichier « dill »
  • exécution de la fonctionnalité
  • suppression du fichier téléchargé

Conclusion

Python se rapproche du Lisp pour sa capacité à faire évoluer son code et ses données. Cela permet de développer en Python des fonctionnalités avancées qui nécessiterait d’interfacer un interpréteur dans des programmes écrits dans d’autres langages.  Pour pus d’articles Python, rendez-vous sur notre blog !

N'oubliez pas de réagir en commentaire