Site WWW de Laurent Bloch
Slogan du site

ISSN 2271-3905
Cliquez ici si vous voulez visiter mon autre site, orienté vers des sujets moins techniques.

Pour recevoir (au plus une fois par semaine) les nouveautés de ce site, indiquez ici votre adresse électronique :

Comparaison de langages de programmation
Python, Scheme, C
Pour l’enseigner à des biologistes (par exemple)
Article mis en ligne le 21 juin 2020
dernière modification le 31 janvier 2024

par Laurent Bloch

Cet article est la transcription de la séance du Séminaire Codes sources que j’ai donnée le 30 janvier 2020, à l’invitation de Baptiste Mélès, pour répondre à la question de savoir quel langage utiliser pour enseigner la programmation. Il fait suite à l’article Python et Biopython.

Functional programming is a radical and elegant attack on the whole enterprise of writing programs. It’s very different from the ’do this and then do that’ programming mentality. You have to rewire your brain in quite a different way.

(Simon Peyton-Jones)

La programmation fonctionnelle est un assaut radical et élégant contre toute la pratique de l’écriture de programmes. Elle s’écarte totalement de la mentalité du programmeur qui « fait ci et puis après fait ça ». Vous devez recâbler votre cerveau d’une façon assez différente.

« Faire ci et puis après faire ça »

C’est la tendance spontanée du programmeur débutant, encouragée par le style de beaucoup de langages, surtout en mode interprété.

En général cela finit de façon horrible, ainsi :

  1. x = int(input("Entrez la valeur de x : "))
  2. y = int(input("Entrez la valeur de y : "))
  3. if x != 0:
  4.     y = x + x**2
  5.     y = (y - 3) // x
  6.     if y != 0:
  7.         y = y % x
  8. print (x, y)

Télécharger

Cette façon de faire est peu satisfaisante. Tous ceux qui utilisent Python pour enseigner sérieusement la programmation sont d’accord sur le fait qu’il faut interdire la fonction input.

Pour construire un programme il faut...

Corrado Böhm et Giuseppe Jacopini on montré en 1966 que tout algorithme pouvait être programmé avec trois constructions :

 séquence d’instructions ;
 alternative ;
 répétitive.

Ce résultat est au fondement de la programmation structurée des années 1970, une étape considérable dans l’art de la programmation, qui énonce qu’une qualité éminente d’un programme sera sa lisibilité par un être humain, et qui préconise de ce fait le renoncement à l’instruction go to.

Leur article est une des deux références de celui de Dijkstra, Go to Statement Considered Harmful (1968) (l’autre est un article de Wirth et Hoare sur Algol).

Environnement de programmation

Voici ce que présente à son utilisateur un environnement de programmation assez représentatif de ce qui se fait.

C’est assez commode d’usage, mais un univers fermé.

Finalement, avec Biopython :

Voici ce que l’utilisateur profane de Biopython est enclin à écrire pour visiter une séquence biologique, ici de protéine, issue de la banque classique SwissProt :

  1. #!/usr/bin/env python3
  2. from Bio import ExPASy
  3. from Bio import SeqIO
  4. with ExPASy.get_sprot_raw("O23729") as handle:
  5.     seq_record = SeqIO.read(handle, "swiss")
  6. print(seq_record.id)
  7. print(seq_record.name)
  8. print(seq_record.description)
  9. print(repr(seq_record.seq))
  10. print("Length %i" % len(seq_record))
  11. print(seq_record.annotations["keywords"])

Télécharger

Biopython répond :

Recherche de séquences

  1. #!/usr/bin/env python3
  2. def FetchSeqMulti(mydb, myrettype, myretmode, myid):
  3.     from Bio import Entrez
  4.     from Bio import SeqIO
  5.     Entrez.email = "lb@laurentbloch.org"
  6.     with Entrez.efetch(db=mydb, rettype=myrettype,\
  7.                        retmode=myretmode, id=myid) as handle:
  8.         for seq_record in SeqIO.parse(handle, "gb"):
  9.             print("%s %s..." % (seq_record.id,\
  10.                                 seq_record.description[:50]))
  11.             print("Sequence length %i, %i features, from: %s"
  12.                   % (len(seq_record), len(seq_record.features),\
  13.                      seq_record.annotations["source"]))
  14.  
  15. FetchSeqMulti("nucleotide", "gb", "text",\
  16.               "6273291,6273290,6273289")

Télécharger

ce qui donne :

Les principales banques de séquences sont :

 GenBank (61 millions de séquences) et EMBL pour les séquences nucléotidiques ;
 UniProt, Swiss-Prot (561 568 séquences) et TrEMBL pour les protéines ;
 PDB, SCOP et CATH fournissent des informations de structure spatiale des protéines, nécessaires aux logiciels de modélisation moléculaire ;
 PubMed est une banque de données bibliographiques.

Code génétique

Le génome est codé dans un alphabet à quatre lettres, A (adénine), C (cytosine), T (thymine), G (guanine) de l’ADN, les nucléotides. Un gène (un ou plusieurs mots du génome), pour être exprimé, doit d’abord être transcrit en ARN messager (même séquence que l’ADN en remplaçant le T de la thymine par U de l’uracile), qui va le transmettre au mécanisme de traduction cellulaire.

Le génome permet la biosynthèse de protéines. Un gène codé dans l’ADN est transcrit en ARN par l’enzyme ARN polymérase, nucléotide pour nucléotide en remplaçant le T (thymine) de l’ADN par le U (uracile) de l’ARN. Ce brin d’ARN messager est ensuite traduit en protéine par le ribosome, un organe de la cellule qui assemble les acides aminés pour former les protéines conformément au code génétique. C’est ainsi que les protéines (éventuellement toxiques pour l’hôte) encodées dans l’ADN d’un virus sont synthétisées par le mécanisme cellulaire de l’hôte.

Les acides aminés constitutifs des protéines présentes dans les organismes vivants sont au nombre de 20. Pour désigner 20 entités différentes avec un alphabet de 4 caractères il faut 3 caractères. Les parties codantes du génome sont donc traduites par groupes de 3, les codons. Un codon de trois nucléotides permettrait de désigner 64 acides aminés, mais il n’y en a que 20 : certains acides aminés sont désignés par plusieurs codons, le code génétique est un code dégénéré.

Acides aminés

Ci-dessous la liste des acides aminés, leurs noms et leurs abréviations, suivis des codons (ici d’ARN) qui codent pour chacun.

Ci-dessous un diagramme, emprunté au professeur Loren Williams, de Georgia Tech, qui résume les principales propriétés des acides aminés :

Avez-vous déjà vu une protéine ?

Une vraie protéine, pas celles que vous trouvez dans les distributeurs de boisson des salles de gymnastique.

En voici une, issu d’un xénope tropical, sorte de crapaud griffu africain aimé des biologistes pour certaines propriétés intéressantes :

Alignement de séquences

Un petit exemple didactique :

  1. from Bio import Align
  2. aligner = Align.PairwiseAligner()
  3. seq1 = "GAACT"
  4. seq2 = "GAT"
  5. score = aligner.score(seq1, seq2)
  6. score
  7. 3.0
  8. alignments = aligner.align(seq1, seq2)
  9. for alignment in alignments:
  10.     print(alignment)

Télécharger

qui donne ceci :

BLAST bien sûr...

Biopython procure un accès commode à BLAST, le logiciel préféré des biologistes (avec EndNote). En fait la force de Biopython est de pouvoir manipuler des données de séquences biologiques par les différents logiciels classiques du domaine en effectuant les conversions de format et de conventions de passage de paramètres sans que le biologiste ait à s’en soucier.

  1. #!/usr/bin/env python3
  2. import sys
  3. from Bio.Blast import NCBIWWW
  4. from Bio.Blast import NCBIXML
  5. def Blast_sonde(blastversion, collection, gi):
  6.     result_handle = NCBIWWW.qblast(blastversion, \
  7.                                    collection, gi)
  8.     blast_record = NCBIXML.read(result_handle)
  9.     E_VALUE_THRESH = 0.04
  10.     for alignment in blast_record.alignments:
  11.         for hsp in alignment.hsps:
  12.             if hsp.expect < E_VALUE_THRESH:
  13.                 print("****Alignment****")
  14.                 print("sequence:", alignment.title)
  15.                 print("length:", alignment.length)
  16.                 print("e value:", hsp.expect)
  17.                 print(hsp.query[0:75] + "...")
  18.                 print(hsp.match[0:75] + "...")
  19.                 print(hsp.sbjct[0:75] + "...")
  20.  
  21. blastversion = sys.argv[1]
  22. collection = sys.argv[2]
  23. gi = sys.argv[3]
  24. Blast_sonde(blastversion, collection, gi)

Télécharger

Si on connaît l’identifiant GenBank, avec la ligne de commande :

./BLAST-biopython.py blastn nt 8332116

BLAST évalue la pertinence statistique du score par une analyse de la distribution des scores d’alignement entre la séquence-test et l’ensemble des séquences cibles, et calcule la probabilité et l’espérance mathématique de trouver un alignement donnant un score donné parmi les cibles, uniquement du fait du hasard. L’espérance mathématique est notée e-value.

Apprend-on ainsi à programmer ?

Non bien sûr, on aligne des recettes sans comprendre leur mécanisme.

 L’expérience IHM (interface humain-machine) nous apprend qu’écrire une instruction est facile ;
 la preuve par le tableur : on apprend facilement à écrire des formules de calcul dans les cases, cependant que le logiciel fournit la charpente qui les assemble de façon cohérente ;
 décomposer un problème plus vaste en sous-problèmes est difficile ;
 d’où la propension à « faire ci et puis après faire ça ».

Ce qu’il faut enseigner, c’est donc la conception et la réalisation de sous-programmes, et leur assemblage.

C’est une bonne raison d’encourager le style fonctionnel, praticable avec la plupart des langages, mais certains l’encouragent plus que d’autres.

Tri Rapide en Python

  1. #!/usr/bin/env python3
  2.  
  3. def tri_rapide (T) :
  4.     imin, imax = 0, len(T) - 1
  5.     tri_part(T, imin, imax)
  6.     return T
  7.  
  8. def tri_part(T, imin, imax) :
  9.     if imin < imax :
  10.         q = partition (T, imin, imax)
  11.         tri_part(T, imin, q)
  12.         tri_part(T, q + 1, imax)
  13.  
  14. def partition (T, imin, imax):
  15.     x = T[imin]
  16.     while True :
  17.         while T[imax] > x :
  18.             imax -= 1
  19.         while T[imin] < x :
  20.             imin += 1
  21.         if imin < imax :
  22.             echange (T, imin, imax)
  23.             imax -= 1
  24.             imin += 1
  25.         else :
  26.             return imax
  27.  
  28. def echange(V, i, j):
  29.     tmp = V[i]
  30.     V[i] = V[j]
  31.     V[j] = tmp
  32.  
  33. import sys
  34. def lire_vecteur(fichier) :
  35.     f = open(fichier, mode = 'r')
  36.     T = f.readlines()
  37.     for i in range(len(T)) :
  38.         T[i] = int(T[i])
  39.     f.close()
  40.     return T
  41.  
  42. print (tri_rapide (lire_vecteur(sys.argv[1])))

Télécharger

Tri Rapide en Scheme

Le programme ci-dessous accepte en paramètre d’entrée le nom d’un fichier qui contient la suite des nombres qui constituent le vecteur à trier. Ces nombres doivent être disposés selon la syntaxe des vecteurs de Scheme, c’est-à-dire précédés des caractères #( et suivis du caractère ).

  1. (module tri-rapide-vecteur
  2.    (main lire-vecteur))
  3.  
  4. (define (lire-vecteur Args)
  5.    (let* ((fichier (cadr Args))
  6.           (influx (let ((s (file->string fichier)))
  7.                      (open-input-string s )))
  8.           (V (read influx))
  9.           (longueurV (vector-length V)))
  10.       (close-input-port influx)
  11.       (multiple-value-bind (res rtime stime utime)
  12.          (time
  13.             (lambda ()
  14.                (tri-rapide V 0 (-fx longueurV 1))))
  15.          (print "real: " rtime " sys: " stime " user: " utime))
  16.       (do ((i 0 (+fx i 1)))
  17.           ((=fx i longueurV))
  18.           (print (vector-ref V i))) ))
  19.  
  20. (define (tri-rapide v imin imax)
  21.   (if (<fx imin imax)
  22.       (let ((q (partition v imin imax)))
  23.         (tri-rapide v imin q)
  24.         (tri-rapide v (+fx q 1) imax)))
  25.   v)
  26.  
  27. (define (partition v imin imax)
  28.   (let ((x (vector-ref v imin)))
  29.     (let loop ()
  30.       (let loop1 ()
  31.         (if (>fx (vector-ref v imax) x)
  32.             (begin
  33.               (set! imax (-fx imax 1))
  34.               (loop1))))
  35.       (let loop2 ()
  36.         (if (<fx (vector-ref v imin) x)
  37.             (begin
  38.               (set! imin (+fx imin 1))
  39.               (loop2))))
  40.       (if (<fx imin imax)
  41.           (begin
  42.             (swap v imin imax)
  43.             (set! imin (+fx imin 1))
  44.             (set! imax (-fx imax 1))
  45.             (loop))
  46.           imax))))
  47.  
  48. (define (swap v i j)
  49.   (let ((x (vector-ref v i)))
  50.     (vector-set! v i (vector-ref v j))
  51.     (vector-set! v j x)))

Télécharger

Comme ce programme était utilisé pour trier des vecteurs de nombres entiers, à l’incitation de Manuel Serrano j’ai utilisé les fonctions d’arithmétique entière explicites =fx +fx -fx <fx >fx, ce qui améliore considérablement les performances. Pour trier des valeurs d’autres types, tels que chaînes de caractères ou nombres en virgule flottante le programme devrait être modifié pour utiliser les fonctions de comparaison appropriées.

Ce programme utilise quelques autres facilités procurées par le compilateur Bigloo, utiles à la manipulation de grands volumes de données comme nous le verrons lorsque nous aborderons la question des mesures de performances.

Tri rapide en C

  1. #include<stdlib.h>
  2. #include<stdio.h>
  3. #include<string.h>
  4. #include <time.h>
  5.  
  6. void quicksort(int tableau[], int premier, int dernier){
  7.   int i, j, pivot, temp;
  8.   if(premier < dernier){
  9.     pivot = premier;
  10.     i = premier;
  11.     j = dernier ;
  12.     while(i < j){
  13.       while(tableau[i] <= tableau[pivot] && i < dernier)
  14.         i++;
  15.       while(tableau[j] > tableau[pivot])
  16.         j--;
  17.       if(i < j){
  18.         temp = tableau[i];
  19.         tableau[i] = tableau[j];
  20.         tableau[j] = temp;
  21.       }
  22.     }
  23.     temp = tableau[pivot];
  24.     tableau[pivot] = tableau[j];
  25.     tableau[j] = temp;
  26.     quicksort(tableau, premier, j-1);
  27.     quicksort(tableau, j+1, dernier);
  28.   }  
  29. }
  30.  
  31. int comptemots(char* fichier){
  32.   FILE *inTube;
  33.   long nombreMots = 0;
  34.   char commande[64] = "wc -w ";
  35.  
  36.   strcat(commande, fichier);
  37.   inTube = popen(commande, "r");
  38.   if (inTube != NULL) {
  39.     fscanf(inTube, "%ld", &nombreMots);
  40.     pclose(inTube);
  41.   }
  42.   //  printf("%ld\n", nombreMots);
  43.   return(nombreMots);
  44. }  
  45.  
  46. int main(int argc, char* argv[]){
  47.   int x, i, nombreMots = 0;
  48.   long clk_tck = CLOCKS_PER_SEC;
  49.   clock_t t1, t2;
  50.   nombreMots = comptemots(argv[1]);
  51.   FILE *fp;
  52.   int *tableau = malloc(nombreMots * sizeof(int));
  53.   fp = fopen (argv[1], "r");
  54.   for (i = 0; i < nombreMots; i++){
  55.     fscanf(fp, "%d", &x);
  56.     *(tableau + i) = x;
  57.   }
  58.   fclose (fp);
  59.   t1 = clock();
  60.   quicksort(tableau, 0, nombreMots - 1);
  61.   t2 = clock();
  62.   for (i = 0; i < nombreMots; i++)
  63.     printf(" %d\n", tableau[i]);
  64.   free(tableau);
  65.   (void)printf("Nb ticks/seconde = %ld,  Nb ticks depart : %ld, "
  66.                "Nb ticks final : %ld\n",
  67.                clk_tck, (long)t1, (long)t2);
  68.   (void)printf("Temps consomme (s) : %lf \n",
  69.                (double)(t2-t1)/(double)clk_tck);
  70.   return 0;
  71. }

Télécharger

Ce programme illustre la raison pour laquelle C n’est pas un langage adapté à l’enseignement de la programmation à des débutants : si la fonction quicksort est intelligible, même si la syntaxe n’est peut-être pas un modèle de lisibilité, main et comptemots sont parfaitement cryptiques. Or c’est avec ce genre de sujets que les informaticiens sont appelés à se débattre quotidiennement. J’ai fait appel à l’aide d’Emmanuel Lazard pour la solution de ces problèmes très techniques.

Quels systèmes de programmation pour l’enseignement ?

La vitesse des programmes produits pour tel ou tel langage avec tel ou tel système de programmation n’est pas le premier critère de choix d’un langage pour l’enseignement, mais il est bon de savoir si ce langage est utilisable de façon réaliste pour traiter des problèmes réels.

Je n’aime pas les environnements de programmation intégrés : ils procurent certes un premier abord facile pour l’étudiant, mais ils lui dissimulent les interactions avec le système d’exploitation et avec les fichiers, or dans la pratique quotidienne de l’informaticien ces interactions sont une des principales sources de difficultés, et il est donc indispensable de s’y frotter. Pour la même raison, il faut que les étudiants compilent leurs programmes, afin d’observer le comportement d’un programme exécutable doté de son vecteur d’état et de ses interfaces avec le monde extérieur. Bref, les interpréteurs sont des logiciels utiles pour mettre le pied à l’étrier, mais cette étape doit être franchie.

Les environnements de programmation intégrés encouragent un style médiocre, le laxisme et l’imprécision dans la déclaration des variables, le déplorable dialogue « entrer les données - calculer - afficher les résultats ».

Mesure de performances du tri rapide (QuickSort)

Pour donner une idée (partielle et rudimentaire) des capacités de quelques systèmes de programmation à traiter des problèmes réels j’ai choisi d’appliquer l’algorithme classique d’Anthony Hoare au tri rapide d’un tableau de 200 millions de nombres entiers tirés au hasard entre 0 et un milliard, avec les programmes dont le texte figure ci-dessus. Voici, à toutes fins utiles, le programme de génération du tableau :

  1. (module creer-vecteur
  2.    (main creer-vecteur))
  3.  
  4. (define (creer-vecteur Args)
  5.    (let* ((taille (string->number (cadr Args)))
  6.           (V (make-vector taille 0))
  7.           (fichier (caddr Args))
  8.           (grandeur
  9.              (if (null? (cdddr Args))
  10.                  10000
  11.                  (string->number (cadddr Args))))
  12.           (flux (open-output-file fichier)))
  13.       (do ((i 0 (+ i 1)))
  14.           ((= i taille)
  15.            (vector-for-each (lambda (n)
  16.                                (display n flux)
  17.                                (display " " flux))
  18.               V)
  19.            (close-output-port flux))
  20.           (vector-set! V i (random grandeur)))))

Télécharger

Sans surprise, Python échoue sans message d’erreur explicite au bout d’une cinquantaine de minutes, mais il serait vain de s’en offusquer : les auteurs du langage préviennent que le langage ne procure pas la récursion terminale, et qu’ils ne garantissent rien au-delà d’une profondeur d’appel récursif de 1000.

De façon plus choquante plusieurs implémentations de Scheme échouent également, ce qui donne à craindre que ce ne soient pas de vrais compilateurs, mais plutôt qu’ils construisent des exécutables en embarquant l’interprète. Cela ne justifie pas vraiment l’échec, que la récursion terminale devrait éviter.

Restent le compilateur Bigloo et, comme point de référence, le compilateur gcc pour C. Le texte des programmes indique où sont pris les points de mesure, j’ai veillé à éliminer les temps d’entrées-sorties. Pour C j’ai essayé plusieurs méthodes, avec rusage et avec clock, il n’y a pas de différence sensible. Voici les résultats :

Le programme C est plus rapide, ce qui est logique parce que c’est un langage de plus bas niveau, mais Bigloo reste dans les mêmes ordres de grandeur, ce qui dénote un vrai compilateur optimisant. De toute façon ces mesures sont intrinsèquement approximatives, ne serait-ce que pour des phénomènes liés à l’usage du cache.

Danger des mesures de performances

Les performances indiquées ci-dessus ont été observées sur un ordinateur particulièrement lent : Acer Aspire XC-100, processeur AMD E1-1200, deux cœurs, 1,4 GHz, 18 W. Ainsi j’ai obtenu des valeurs numériques plus grandes, donc une meilleure précision. Et surout cela n’a pas trop chauffé.

J’ai imprudemment fait tourner ces programmes sur un petit ordinateur portable doté d’un processeur Intel Core I7 7500U, deux cœurs, 2,7 GHz. Mal m’en a pris : ultra-mince, mal ventilé, la chaleur dégagée par l’appareil a détérioré les lignes de commande de l’écran non démontable, il faudrait changer tout le dos, la réparation coûterait pratiquement le prix de l’ordinateur. Pensez-y quand vous ferez vos propres essais !

Avantages du style fonctionnel

 Le ralentissement des progrès de la vitesse des processeurs, le
recours aux GPU et d’autres facteurs stimulent le recours au calcul
parallèle ;
 garantir qu’une tâche ne modifiera pas l’état du système pris
comme hypothèse par une autre tâche est nécessaire au déterminisme
du calcul ;
 le style fonctionnel limite le recours à l’affectation et par là facilite
la satisfaction de cette condition.

Le marché florissant de la lambda-expression

Expedia did “over 2.3 billion Lambda calls per month” back in December 2016. That number jumped 4.5 times year-over-year in 2017 (to 6.2 billion requests) and continues to rise in 2018. Example applications include integration of events for their CI/CD platforms, infrastructure governance and autoscaling.

https://aws.amazon.com/serverless/v...