2 Ce qu’il vous faut assimiler à 100% le plus tôt possible

2.1 Pour commencer

Ici nous allons aborder les éléments les plus centraux, parfois frustres, permettant de transcrire un algorithme simple en Python par l’usage d’un Python on ne plus économe.

2.1.1 Python, comme une calculette

Vous pouvez utiliser Python comme une calculette triviale (\(*\) représente la multiplication, \(**\) la puissance).

a = 1
b = 3
c = 2*a + b**2
print(c)
--|  11

Pour intégrer dans votre code les objets qui ne sont pas présent par défaut, il vous faut les importer.

# numpy est un package comportant de nombreuses fonctions de calcul
import numpy as np # une des syntaxes d'importation

Pour avoir une idée du nombre de fonctions disponibles dans \(numpy\) taper ceci.

dir(np)

NB :la liste étant très longue, nous ne l’avons pas mis dans ce document.

Votre calculette possède maintenant de nombreuses nouvelles fonctions, ici la valeur absolue, qui n’a qu’un seul paramètre (l’argument) et l’arrondi qui possède deux arguments : le réel à arrondir et le nombre de chiffre après la virgule.

print(np.abs(-10.45))       # fonction valeur absolue
--|  10.45
print(np.round(11.78945,2)) # arrondi à deux 
--|  11.79

2.1.2 Faire des choix

Pour affecter une valeur en fonction d’une condition unique la syntaxe la plus efficace est la suivante : resultat = (expression_if_true if condition else expression_if_false).

Par exemple pour dire que si x est strictement négatif alors on veut obtenir 0 et sinon on veut obtenir 10, on ferait:

x  = -50
y  = (0 if x < 0 else 10)
print(x,y)
--|  -50 0
x  = 100
y  = (0 if x < 0 else 10)
print(x,y)
--|  100 10

Avec cette syntaxe les parenthèses ne sont pas obligatoires, mais améliorent la lisibilité.

Quand vous voulez effectuer une opération en fonction d’une liste de conditions, il faut enchaîner des \(if\), \(elif\) et \(else\) en fonction du nombre de conditions que vous voulez tester.

Ici on a trois conditions explicites et une quatrième pour rassembler tous les autres cas de figure, essayez de faire fonctionner ce code avec diverses valeurs pour \(temperature\).

temperature = 30

if    19 <= temperature <= 21 :
      print("température raisonnable")
elif  13 <= temperature < 19  :
      print("il fait frais")
elif  21 < temperature <= 28  :
      print("il fait un peu chaud")
else  :
      print("température pas agréable")
      
--|  température pas agréable

Quand on n’a qu’une alternative on n’utilise que le \(if\), avec deux alternatives on utilise le \(if\) et le \(else\).

2.1.3 Créer ses propres fonctions

Dans ce paragraphe, nous nous contenterons d’une vue minimaliste des fonctions : un nom de fonction,une documentation de la fonction, un ou plusieurs arguments non optionnels en entrée qui ne seront pas modifiés, un résultat.

Dans l’idéal ces fonctions ne traitent et ne produisent que des types Python natifs (“build-in”), en particulier les entiers (int), les réels(float), les booléens, les chaînes de caractères (string), les tuples, les ensembles, les dictionnaires et les listes (que nous verrons plus loin) ou des combinaisons/imbrications de ces types build-in.

Par ailleurs, if faudrait que le résultat de ces fonctions ne dépende que ses arguments entrants et pas d’autres informations captées directement au sein de la fonctions.

Quand vous le pouvez, astreignez vous à créer ce type de fonctions que l’on nomme fonctions “pures”, proches d’une fonction mathématique.

Les fonctions comportant des I/O (entrées/sorties), par exemple des saisies (input) des affichages(print) ou impressions, ayant des actions externes de lecture/écriture dans des fichiers ou des base de données, ne sont pas des fonctions pures. Mais on en a souvent besoin. La stratégie est donc de bien isoler les fonctions pures puis de les utiliser dans les autres fonctions où l’on essaiera de se rapprocher du style “fonctions pures”. Les fonctions crées dans des objets, appelées méthodes, que nous verront plus loin, peuvent être un peu moins pures au sens où elles utilisent directement des variables internes à l’objet.

Nous allons transformer en fonction un exemple donné plus haut. Le nom de la fonction sera test_temperature et la documentation intégrée de la fonction se trouvera entre deux triples cotes. Ce type de documentation se nome “docstring” et permettra la création de documentation automatique, de tests automatiques et la création d’une aide (help) sur la fonction. Nous verrons plus loin que nous pouvons aussi documenter de la même façon les classes des objets.

# création d'une fonction pure et documentée

def test_temperature(temperature) :
  """ 
  Evalue le ressenti en fonction de la température ambiante.
  
  Parameters :
    temperature (int ou float) : température ambiante
  
  Returns :
    string : ressenti en Français
  
  Tests doctest intégrés (résultats attendus sur certains cas, à compléter !) :
    >>> test_temperature(20)
    'température raisonnable'
    
    >>> test_temperature(0)
    'température pas agréable'
    
  """ 
  
  # le corps de la fonction
  if    19 <= temperature <= 21 :
        ressenti = "température raisonnable"
  elif  13 <= temperature < 19  :
        ressenti = "il fait frais"
  elif  21 < temperature <= 28  :
        ressenti = "il fait un peu chaud"
  else  :
        ressenti = "température pas agréable"
        
  return ressenti # le résultat retourné par la fonction   

On peut maintenant utiliser cette fonction à notre convenance.

print(test_temperature(19.5))
--|  température raisonnable

Remarque : En ajoutant le code suivant à la fin du fichier où vous définissez vos fonctions, vous pouvez lancer tous les tests unitaires décrits dans votre doctest. En fait, à la fin de chaque fichier Python, vous incorporez un test pour déterminer si c’est ce fichier à partir duquel a lancé l’exécution (via le test if __name__ == ‘__main__’ :). Avec le code suivant, vous demandez à ce que l’exécution de ce fichier lance les tests unitaires. L’idée étant que l’utilisation réelle de vos fonctions se fera via un autre fichier python qui aura son propre “main”. C’est que nous testerons dans le paragraphe suivant qui introduira les modules.

if __name__ == '__main__':
    import doctest
    doctest.testmod()

Essayer d’effectuer une petite modification dans une des réponses au test située dans la chaîne de documentation (>>>) et vous constaterez que grâce à “testdoc” apparaîtra une mention “test failure” comme suit

$ python fonction_avec_text_doc.py
**********************************************************************
File "fonction_avec_text_doc.py", line 13, in __main__.test_temperature
Failed example:
    test_temperature(20)
Expected:
    'température raisonnableX'
Got:
    'température raisonnable'
**********************************************************************
1 items had failures:
   1 of   2 in __main__.test_temperature
***Test Failed*** 1 failures.

Dans votre éditeur Python, s’il est compatible avec le help Python, vous pouvez maintenant taper ceci pour obtenir de l’aide sur la fonction.

help(test_temperature)
--|  Help on function test_temperature in module __main__:
--|  
--|  test_temperature(temperature)
--|      Evalue le ressenti en fonction de la température ambiante.
--|      
--|      Parameters :
--|        temperature (int ou float) : température ambiante
--|      
--|      Returns :
--|        string : ressenti en Français
--|      
--|      Tests doctest intégrés (résultats attendus sur certains cas, à compléter !) :
--|        >>> test_temperature(20)
--|        'température raisonnable'
--|        
--|        >>> test_temperature(0)
--|        'température pas agréable'

Vous obtenez alors votre texte de documentation.

2.1.3.1 Portée des variables et passage de paramètres


Les variables déclarées au sein de la fonction sont locales, c’est à dire non accessibles à l’extérieur de celle-ci. On parle de la portée d’une variable. Les variables déclarées plus haut dans la hiérarchie du programme (i.e. à l’extérieur de la fonction) sont des variables globales. Il est rarement judicieux et toujours dangereux en terme de d’anomalies potentielles d’utiliser des variables globales dans vos fonctions, sauf pour des paramètres globaux du contexte d’exécution de votre programme. Proscrivez l’usage consistant à faire évoluer les variables globales dans une fonction, sauf si cette fonction est explicitement dédiée à ce rôle.

Les fonctions qui retournent de nouvelles valeurs aux paramètres qui leur sont passés (= change ces valeurs) et/ou qui on un nombre variable d’arguments, sont en fait des “procédures” ; sauf si vous avez une bonne raison, comme créer une procédure “outil” destinée à traiter de nombreux cas de figure différents et qui sera utilisé dans plusieurs contextes différents, évitez ce style de fonction qui relèvent de techniques de programmation vieillottes et susceptibles d’introduire de nombreuses anomalies par effets de bord. En tout état de cause, les procédures sont plus difficiles à tester, doivent être parfaitement documentées et testées et se retrouvent souvent dans des packages bien versionnés et partagés dans une communauté de développeurs.

2.1.3.2 Mourir vite


Quand une condition, un résultat attendu, n’est pas conforme à ce qui doit être dans un programme (i.e. c’est un imprévu potentiellement nocif) il convient d’informer et de stopper le programme.

Pour gérer l’arrêt du programme, vous pouvez vous inspirer du code suivant, où vous remarquerez l’usage de assert qui stoppe le programme si on lui introduit la valeur False, mais avant émet ici une chaine de caractère pour expliquer ce qui c’est passé.

def fonction_test_erreur() :
  ''' Ici on gère une ou plusieurs erreurs '''
  
  erreur = False # pas d'erreur a priori
  
  # ici placer le ou les tests, pour constater l'erreur éventuelle
  
  # ici renseigner le contexte et l'erreur
  erreur          = True
  erreur_lib      = "erreur x, mauvais traitement"
  erreur_context  = "cela ce passe ici"
  
  return erreur, erreur_lib, erreur_context

e,l,c = fonction_test_erreur()
assert not e, f'il y a une {l}, dans le contexte {c}' 

Ce qui remonte l’erreur suivante (en anglais : “to raise an error”)

AssertionError: il y a une erreur x, mauvais traitement , dans le contexte cela ce passe ici

Avant de stopper le programme, on aurait pu écrire l’erreur dans un fichier ou système de “logging” (traces de l’exécution d’un programme).

2.1.4 Créer ses propres bibliothèques de fonction

Pour créer votre propre bibliothèque, que l’on nomme un “module”, rien de plus simple :

  1. Créer un fichier, avec vos fonctions en conservant l’appel à testdoc dans sa partie main, car ce main là n’est invoqué que si vous exécutez directement le fichier ce fichier module.

  2. Créer votre programme en commençant par importer le module sous une abréviation courte et en stipulant son code (qui sans doute appelle une fonction du module !) dans sa partie main.

Par exemple : nous avons créé un fichier module1.py comprenant le code vu plus haut avec la définition de la fonction test_temperature suivie de l’appel à doctest mentionné plus haut, puis nous avons créé le programme suivant :

import module1 as m1

if __name__ == '__main__':

  print("""
  Ce programme invoque la fonction test_temperature()
  du module module1.py
  """)
  
  for t in -1,13,19,20,28 : 
    m1.test_temperature(t)

Son exécution donne ceci.

Ce programme invoque la fonction test_temperature()
  du module module1.py
  
'température pas agréable'
'il fait frais'
'température raisonnable'
'température raisonnable'
'il fait un peu chaud'

Un ensemble de modules forme un paquet (package).

2.1.5 Vos deux premiers types build-in composés

2.1.5.1 Les t_uplets (tuple)


Les t_uplets (ou p_uplets pour certains mathématiciens), à savoir une suite ordonnée d’objets, représente le type construit à partir d’autres type le plus basique de Python. Il ne faut pas les confondre avec les listes que nous verrons plus loin.

Les t_uplets connus de tous sont les couples et les triplets.

Bien que rustique, cette structure est une des plus efficaces en terme de performances (rapidité d’exécution), bien plus rapide que les listes que nous verrons plus loin.

Syntaxiquement, il suffit d’énumérer les objets avec des virgules pour les séparer (vous pouvez entourer cette suite ou pas de parenthèses).

u = 1, 3, 4, 4, 0, 1  # un t_uplets
u
--|  (1, 3, 4, 4, 0, 1)

Vérifions le type de l’objet \(u\) est bien le type espéré :

print(type(u))
--|  <class 'tuple'>

Essayons avec des parenthèses et en y mettant divers objets, comme des chaînes de caractères (strings).

us = ("un", "deux","trois")  # un t_uplets de string
print(us)
--|  ('un', 'deux', 'trois')
print(type(us))
--|  <class 'tuple'>

On constate qu’avec ou sans parenthèse on obtient un tuple.

Essayons des réels.

ur = 1.58 , np.pi , -125.7, 10e-4, 10e2 
print(ur)
--|  (1.58, 3.141592653589793, -125.7, 0.001, 1000.0)

Dans l’exemple précédent, remarquez comment nous avons pu utiliser la définition du nombre pi contenue dans le paquet numpy et la façon d’écrire les puissances de 10.

Essayons les expressions booléennes.

ub = True,False,not True,1 == 1,1 == 2,1 < 2,1 >= 2,True & True,False | True  
print(ub)
--|  (True, False, False, True, False, True, False, True, True)

Voyons comment créer un tuple vide. Là les parenthèses sont importantes !

v = ()   
print(v)
--|  ()

Et maintenant voyons comment créer un tuple avec un seul élément. Bizarrement, c’est ce cas de figure qui questionne le plus un développeur Python débutant !

u1 = 666,     # Attention si l'on ne met pas la virgule on obtient un entier !
print(u1)
--|  (666,)
print(type(u1))
--|  <class 'tuple'>

Pour créer un tuple contenant une suite d’entier, il faut utiliser la fonction \(range\).

tuple(range(5))       # les entiers de 0 à 4 (la dernière position est exlue)
--|  (0, 1, 2, 3, 4)
tuple(range(1,5))     # les entiers de 1 à 4
--|  (1, 2, 3, 4)
tuple(range(-3,10,2)) # les entiers  de -3 à 9, mais de deux en deux
--|  (-3, -1, 1, 3, 5, 7, 9)

Reprenons notre tuple u, et calculons le nombre d’objet qu’il contient.

n = len(u)
print(n)
--|  6

Pour ajouter un ou plusieurs éléments il suffit d’utiliser le symbole \(+\).

u = u + u1
n = len(u)
print(u)
--|  (1, 3, 4, 4, 0, 1, 666)
print(n)
--|  7

Pour créer un tuple qui correspond à l’agrégation de plusieurs tuple, il faut utiliser le symbole \(*\) (= plusieurs \(+\)). Attention, cela n’effectue aucune multiplication dans le tuple !

u3 = 3*u
print(u3)
--|  (1, 3, 4, 4, 0, 1, 666, 1, 3, 4, 4, 0, 1, 666, 1, 3, 4, 4, 0, 1, 666)

Pour créer un t_uplet vide, rien de plus simple

v = ()
print(type(v)) # c'est bien un tuple
--|  <class 'tuple'>

Voyons toutes les fonctions attachées à l’objet u, parce que c’est un tuple.

dir(u)
--|  [  '__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__',
--|     '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__',
--|     '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__',
--|     '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__',
--|     '__reduce__', '__reduce_ex__', '__repr__', '__rmul__', '__setattr__',
--|     '__sizeof__', '__str__', '__subclasshook__', 'count', 'index']

Oh ! ces fonctions sont nombreuses, comme elles sont attachées à l’objet \(u\) de classe tuplet, on les nomme “méthodes”.

Voyons ce que nous pouvons facilement faire avec certaine méthodes de notre tuple \(u\).

u.__getitem__(0) # premier élément (on commence à zéro !)
--|  1
u[0]             # idem
--|  1
u[1]             # deuxième éléments
--|  3
u.count(1)       # nombre de fois où l'élément 1 est présent
--|  2
u.index(4)       # index de la première position contenant l'élément 4
--|  2
u[-1]            # dernier élément
--|  666

Les tuples sont immuables, vous ne pouvez pas affecter une nouvelle valeur à une position, \(u[1] = b\) n’est pas possible. Mais on peut fabriquer un autre tuple de même nom à partir d’un premier.

b = 25           # créer un nouveau tuple u avec un élément supplémentaire
u = u + (b,)    
u
--|  (1, 3, 4, 4, 0, 1, 666, 25)

Le tuple est la structure “itérable” la plus simple pour contrôler une boucle.

for e in u : print(e**2) # les carrés des valeurs contenues dans u
--|  1
--|  9
--|  16
--|  16
--|  0
--|  1
--|  443556
--|  625

Si vous effectuez plusieurs calculs dans une fonction, retournez un tuple de la façon suivante : return resultat1, resultat2, resultat3. Cela n’affectera pas la pureté de la fonction.

Vous pouvez aussi utiliser un tuple pour contrôler un choix.

if 666 in u : print("il faut faire quelque chose")
--|  il faut faire quelque chose

2.1.5.2 map et lambda


Nous introduisons ici la très importante fonction map, qui vous permet d’appliquer une fonction sur chacune des positions d’un itérable sans écrire de boucle. Ce qui est lisible, facile et performant.

Voyons comment appliquer une fonction \(f(x)\) pour obtenir un nouveau tuple de valeurs augmentée de 10%.

def f(x) : return x * 1.10  # la fonction qui sera appliquée sur chaque élément
m = tuple(map(f , u))       # le mapping de f sur u, et son casting en tuple
print(m)
--|  (1.1, 3.3000000000000003, 4.4, 4.4, 0.0, 1.1, 732.6, 27.500000000000004)

En introduisant l’instruction \(lambda\), l’instruction phare de la programmation fonctionnelle, vous n’avez plus besoin de nommer la fonction à appliquer.

m = tuple(map(lambda  x : x * 1.1 , u))
print(m)
--|  (1.1, 3.3000000000000003, 4.4, 4.4, 0.0, 1.1, 732.6, 27.500000000000004)

Attachez-vous à suivre le conseil de style décrit maintenant.

Quand vous avez besoin de créer des calculs très imbriqués, résister à la tentation d’écrire des expressions du genre :

def f(x) : return np.sin(x * np.cos(x**2 + 2** x -1 ))
print(f(1))
--|  -0.4042391538522658

Mieux vaux procéder par étapes et créer un ou plusieurs “pipe” :

  1. identifiez les expressions les plus basses dans l’ordre les imprications de parenthèses, ici (x2 + 2 x -1 ) et effectuer ces calculs en premier : step1 = x2 + 2 x -1

  2. exprimez le niveau de de parenthèse suivant en utilisant step1 : np.sin(x * np.cos(step1))

  3. et ainsi de suite …

Ici cela donne une expression dont vous pouvez le cas échéant analyser la construction et les résultats pas à pas

def f(x) :
  step1 = x**2 + 2** x -1
  step2 = x * np.cos(step1)
  step3 = np.sin(step2)
  return step3
print(f(1))
--|  -0.4042391538522658

Pendant la mise au point de votre fonction, cela vous permet de vérifier les résultats intermédiaires. Il n’y a pas de différence notable de performance.

Vous pouvez aussi utiliser cette technique pendant une session de refactoring, à savoir l’amélioration de votre code sans en changer les fonctionnalités.

Pour appliquer votre fonction sur une structure itérable comme un tuple, utilisez map (ou un générateur que nous verrons plus loin).

f_de_u = tuple(map(f,u))

2.1.5.3 les ensembles (set)

/

Les ensembles (set) sont des structures où l’on ne trouve qu’une occurrence d’un élément donné. On peut leur appliquer les fonctions mathématiques habituelles des ensembles (intersection, union …). Ils sont également très utiles pour s’assurer que l’on a bien créer une structure sans doublon.

E1  = {6,2,3,1}
E2  = {3,6,12}

cardinal_E1 = E1.__len__()
print(cardinal_E1)
--|  4
E1.__contains__(6)
--|  True
E3 = E1.intersection(E2)
E4 = E1.union(E2)
E5 = E1.symmetric_difference(E2)

for E in (E1,E4,E5): print(E) # lister collection d'ensembles
--|  {1, 2, 3, 6}
--|  {1, 2, 3, 6, 12}
--|  {1, 2, 12}
e1_1 = E1.pop()
print("premier pop : ", e1_1, ", reste :",E1)
--|  premier pop :  1 , reste : {2, 3, 6}
e1_1 = E1.pop()
print("deuxième pop : ", e1_1, ", reste :",E1)
--|  deuxième pop :  2 , reste : {3, 6}

On utilise souvent les ensembles pour supprimer les doublons d’un autre type de données, par exemple d’une liste.

l = [5,9,4,5,6,6]
E = set(l)
print(E)
--|  {9, 4, 5, 6}

Puis, on peut reconstituer une liste maintenant dédoublée.

l_dedouble = list(E)

print(l_dedouble)
--|  [9, 4, 5, 6]

2.2 Manipulations de données courantes

2.2.1 Utiliser la fonction print et découvrir les strings

Les chaînes de caractère peuvent être crées avec de simples cotes : \('blabla'\) , ou doubles cotes : \("blabla"\).

On peut les utiliser dans la fonction \(print\). Si le message comporte lui-même une cote ou double cote il faut la faire précéder du caractère \(\backslash\) (c’est ce que l’on nomme un caractère d’échappement).

Les chaînes de caractères sont immuables et leur type est nommé \(string\).

print("Une chaîne de caractère avec une \" et une \'. ")
--|  Une chaîne de caractère avec une " et une '.

Pour créer des chaînes comportant plusieurs lignes il faut les encadrer avec trois cotes ou doubles cotes.

print(
  '''
  Ceci est un texte 
  avec plusieurs lignes
  '''
)
--|  
--|    Ceci est un texte 
--|    avec plusieurs lignes
--|  

On peut également procéder comme dans d’autres langages pour générer des sauts de ligne, en utilisant la séquence d’échappement \(\backslash n\).

print("une première ligne\navec une deuxième ligne et un saut final\n")
--|  une première ligne
--|  avec une deuxième ligne et un saut final

Les chaînes de caractères sont des objets qui peuvent être manipulés au travers de différentes méthodes, à savoir des fonctions propres à une classe d’objet. L’une d’elle est la méthode \(format\) qui recherche les séquences comme \(\{0\}\) dans une chaîne et les remplace par une autre chaîne de caractère en utilisant le numéro d’ordre entre accolades. Pour invoquer une méthode sur un objet on utilise le point “\(.\)” .

print(
  "première insertion : {0}, deuxième insertion : {1}, bis première : {0}"
  .format("une chaîne",666) 
  )
--|  première insertion : une chaîne, deuxième insertion : 666, bis première : une chaîne

On peut ne rien mettre au sein des accolades, auquel cas l’ordre est implicite.

print(
  "première insertion : {}, deuxième insertion : {}, bis première : {}"
  .format("une chaîne",666, "une chaîne") 
  )
--|  première insertion : une chaîne, deuxième insertion : 666, bis première : une chaîne

Au lieu d’utiliser un numéro d’ordre on peut nommer les champs qui seront renseignés (ici on parlerait de placeholders). Cette façon de procéder est plus verbeuse, mais souvent plus solide en terme de programmation professionnelle, car elle résiste à des modifications ultérieurs de l’expression utilisée; en effet on élimine ici le risque de se tromper dans l’invocation des variables du fait qu’elles soient explicitement nommées.

print(
  "première insertion : {chaine}, deuxième insertion : {nombre}, bis première : {chaine}"
  .format(chaine ="une chaîne",nombre = 666) 
  )
--|  première insertion : une chaîne, deuxième insertion : 666, bis première : une chaîne

Evidemment, ces valeurs peuvent être passées en tant que variables ou constantes nommées.

c1 = "une chaîne"
n1 = 666
print(
  "première insertion : {chaine},deuxième insertion : {nombre}, bis première : {chaine}"
  .format(chaine = c1,nombre = n1)
  )
--|  première insertion : une chaîne,deuxième insertion : 666, bis première : une chaîne

Pour créer une chaîne pas à pas on peut également concaténer plusieurs chaînes.

c1 ="début"
c2 = "fin"
blanc = " "
ma_chaine = c1+blanc+c2
print(ma_chaine)
--|  début fin

Il est possible d’extraire une chaîne de caractères d’une autre chaîne (en anglais de façon générale on dit subsetting ou slicing et on obtient une substring).

Pour comprendre la syntaxe, il faut tout d’abord identifier que les indices, à savoir les positions dans les numéros d’ordre dans les énumérations Python commencent à \(0\) (contrairement au langage R, où elles commencent à \(1\)). En Python les crochets ouvrants/fermants se charge d’extraire les informations d’une séquence.

c = "abcdef"
print("l'élément en première position, c.a.d. la position 0 : ",c[0]) 
--|  l'élément en première position, c.a.d. la position 0 :  a
print("les éléments en positions 0,1,2,3 : ",c[0:4]) 
--|  les éléments en positions 0,1,2,3 :  abcd

On remarque que \(c[0:4]\) signifie : de la position \(0\) à la position \(3\), à savoir \(4-1\) puisque l’on commence à la position \(0\), ce qui peut rapidement se lire comme étant les quatre premières positions.

On peut appliquer diverses méthodes aux chaînes.

c = "une expression"
print("Ajout d'une majuscule : {}".format(c.capitalize()))
--|  Ajout d'une majuscule : Une expression

On peut être amené à déterminer si la chaîne correspond à une valeur numérique, par exemple avant de l’accepter comme une saisie valide par un utilisateur.

# tester si la chaîne peut être interprétée comme une valeur numérique
print("c'est une valeur numérique :",c.isnumeric())
--|  c'est une valeur numérique : False
c = "777"
print("c'est une valeur numérique :",c.isnumeric())
--|  c'est une valeur numérique : True

Nous allons maintenant fabriquer un autre type de séquence : une \(list\)

# transformer naïvement la chaîne en une liste de mots
c = "Je suis belle Ô mortels"
l = c.split()
print(l)
--|  ['Je', 'suis', 'belle', 'Ô', 'mortels']

Au travers de cette dernière méthode nous avons créé un objet d’un type nouveau : une liste (ici une liste de strings).

On peut retrouver la liste d’origine en appliquant la méthode \(join\) de la liste à une “petite” \(string\) contenant un séparateur. Cette syntaxe peut sembler un peu étrange, mais la logique en est claire : pour obtenir une \(string\) on utile une autre \(string\) que l’on transforme par une méthode dédiée.

c_new = " ".join(l) 
print(c_new)
--|  Je suis belle Ô mortels

2.2.2 Utiliser les listes

Comme les strings, les list sont des séquences, mais alors que les strings sont constituées de caractères, les listes peuvent contenir tout type d’objet, par exemple une liste de listes. Les éléments d’une liste n’ont pas besoin d’être de même type.

Le subsetting utilisé pour constituer une sous-liste est identique à celui vu plus haut, ici nous allons extraire les trois premiers mots de notre liste \(l\).

print(l[0:3])
--|  ['Je', 'suis', 'belle']

On dispose d’une méthode pour obtenir le nombre d’éléments d’une séquence, ici de notre liste.

n = l.__len__()  #  n = len(l) procure le même résultat en encapsulant cette
                 #  méthode
                 
print("nombre d'éléments de la liste : ", n)
--|  nombre d'éléments de la liste :  5

Maintenant il est facile de constituer une liste des derniers éléments de notre liste, ici nous allons extraire deux éléments.

print(l[n-2:n])
--|  ['Ô', 'mortels']

Cette syntaxe avec les “\(:\)” comporte d’autres facilités, on peut par exemple omettre des valeurs et obtenir alors des valeurs par défauts et ajouter un incrément positif ou négatif pour définir le parcours de la liste. Il vous faut absolument retenir le subsetting suivant qui vous permet d’inverser l’ordre d’une liste.

print(l[::-1])
--|  ['mortels', 'Ô', 'belle', 'suis', 'Je']

Pour savoir si une valeur apparaît au moins une fois dans une séquence, on peut utiliser \(in\).

"mortels" in l    # vrai
--|  True

A l’inverse, pour savoir si une valeur n’apparaît jamais dans une séquence, on peut utiliser \(not \space in\).

"mortels" not in l    # faux
--|  False

Pour compter le nombre d’occurrences d’un élément d’une liste ou d’une chaîne.

l.count("mortels") # une occurrence 
--|  1

Pour identifier la position (premier index) d’un élément.

l.index("mortels") 
--|  4

2.2.3 Créer et transformer des listes

2.2.3.1 Les bases


Pour insérer un élément avant une position.

l.insert(4,"petits")
print(l)
--|  ['Je', 'suis', 'belle', 'Ô', 'petits', 'mortels']

Pour insérer un élément en dernière position.

l.append("singuliers")
print(l)
--|  ['Je', 'suis', 'belle', 'Ô', 'petits', 'mortels', 'singuliers']

Pour supprimer un élément en fonction de sa position (par défaut celui de la dernière position) et récupérer l’élément en question.

e = l.pop()
print(" contenu extrait : ",e)
--|   contenu extrait :  singuliers
print(l)
--|  ['Je', 'suis', 'belle', 'Ô', 'petits', 'mortels']

Pour supprimer la première occurrence d’un élément.

l.remove("petits")
print(l)
--|  ['Je', 'suis', 'belle', 'Ô', 'mortels']

Avant un algorithme, il convient souvent d’initialiser intelligemment les variables utilisées, dans le cas des listes il est souvent judicieux de créer une liste vide.

lv = list() # une liste vide, on aurait pu écrire lv = []
print(lv)
--|  []

Attention, contrairement au comportement que l’on trouve dans d’autres langages le signe \(=\) entre deux listes n’effectue pas une copie de l’une dans l’autre, mais se contente de créer un alias. Si l’on veut une copie il faut utilser la méthode \(copy\).

# créer un alias
ma_liste = [0,10,20]
un_alias = ma_liste
ma_liste.append(30)
print("ma_liste : ",ma_liste)  
--|  ma_liste :  [0, 10, 20, 30]
print("un_alias : ",un_alias)      # un_alias et ma_liste ne font qu'une
--|  un_alias :  [0, 10, 20, 30]
# copier une liste dans une autre

copy_liste = ma_liste.copy()       # une vraie copie
ma_liste.append(40)
print("ma_liste   : ",ma_liste)    # les listes évoluent de façon indépendante 
--|  ma_liste   :  [0, 10, 20, 30, 40]
print("copy_liste : ",copy_liste) 
--|  copy_liste :  [0, 10, 20, 30]

On a le même comportement avec les \(string\) : en règle générale, si vous identifiez une méthode \(copy\) pour un type d’objet, utilisez la pour effectuer une copie !

2.2.3.2 Effectuer une boucle sur une liste


Pour effectuer une boucle à partie d’un iterable, ici une liste, rien de plus simple :

for valeur in ma_liste : print(valeur)
--|  0
--|  10
--|  20
--|  30
--|  40

Mais souvent, nous avons besoin de la position dans la liste. Au lieu de gérer vous même un compteur dans la boucle vous devez utiliser la fonction \(enumerate\), cela évide de nombreuses erreurs courantes et améliore la lisibilité pour les professionnels de Python.

Cette fonction retourne simultanément deux variables, ce qui peut surprendre, mais ce qui est justement la force. La première valeur représente la position dans l’itérable, la deuxiène la valeur contenue dans l’itérable.

for position, valeur in enumerate(ma_liste) : 
  print(f"position : {position}, valeur : {valeur}")
--|  position : 0, valeur : 0
--|  position : 1, valeur : 10
--|  position : 2, valeur : 20
--|  position : 3, valeur : 30
--|  position : 4, valeur : 40

Si, plutôt que la position (qui commence à zéro), vous voulez compter les boucles, vous ajoutez le paramètre \(start =1\).

for i, v in enumerate(ma_liste, start = 1) : 
  print(f"boucle : {i}, valeur : {v}")
--|  boucle : 1, valeur : 0
--|  boucle : 2, valeur : 10
--|  boucle : 3, valeur : 20
--|  boucle : 4, valeur : 30
--|  boucle : 5, valeur : 40

2.2.3.3 Manipulation vectorielle d’une liste


Nous allons maintenant aborder un point clé : comment modifier le contenu d’une liste en appliquant une fonction à chaque élément de la liste, sans gérer une boucle ?

Vous trouverez plusieurs styles d’écriture pour effectuer ce type d’action : utilisation de la fonction \(map()\), listes en compréhensions et utilisation d’un générateur. Les syntaxes sont proches quand on lit le code correspondant, mais nous vous conseillons d’utiliser les générateurs, dont le style est “pythonique”, qui sont efficaces en terme de performances et généralisables à d’autres contextes. L’idée est la suivante : on définit ce que l’on veut faire (le générateur) et on l’utilise pour créer la liste.

def f(x) : return 2*x+1                 # une fonction quelconque

definition = (f(x) for x in ma_liste)   # objet générateur de la liste
f_liste    = list(definition)           # création de la liste
print(f_liste)
--|  [1, 21, 41, 61, 81]

Pour créer ce type de générateur à partir d’une fonction appliquée à chaque élément d’un itérable, ici une liste, on est souvent confronté au problème suivant : la fonction que nous voulons appliquer possède plusieurs arguments et on voudrait donc fixer leur fixer une valeur. Pour ce faire il faut utiliser la fonction \(partial\) du paquet \(functools\).

Comme exemple nous allons construire la fonction \(ReLU\) (pour Rectified Linear Unit), fonction utilisé dans les réseaux neuronaux comme fonction d’activation des neurones. Cette fonction se construit trivialement à partir de la fonction \(max\). C’est la fonction qui renvoie \(0\) pour \(x < 0\) et \(x\) sinon.

from functools import partial

ReLU = partial(max,0)  # une nouvelle fonction dont le résultat est >= 0
ReLU(-1)
--|  0
ReLU(10)
--|  10

La fonction ReLU n’a qu’un argument, on pourra donc l’utiliser pour construire facilement un générateur et obtenir sa valeur pour toute une liste.

l = [-100,-10,-1,0,10,20,30,70]
g = (ReLU(x) for x in l)   # objet générateur de la liste
ReLU_liste    = list(g)    # création de la liste
print(ReLU_liste)
--|  [0, 0, 0, 0, 10, 20, 30, 70]

On est souvent amené à changer le type d’éléments contenus dans une liste, par exemple transformer une liste de chaînes de caractère représentant des numériques en valeurs numériques. En anglais on parle de type casting. Les fonctions habituelles effectuant cela sont : \(int, float, bool, str\) qui respectivement permettent de créer des entiers, des réels, des booléens, des strings à partir d’autres types.

Le code suivant utilise un générateur pour transformer une liste de string en liste de réels.

liste_a_caster = ["10","9","8","7","6"]
g = (float(x) for x in liste_a_caster)
liste_num  = list(g) # équivalent à : list(map(float, liste_a_caster))
print(liste_num)
--|  [10.0, 9.0, 8.0, 7.0, 6.0]

La somme des éléments d’une liste numérique est facile à obtenir.

somme = sum(liste_num)
print("somme de la liste : ",somme)
--|  somme de la liste :  40.0

Il est très aisé d’effectuer des statistiques basiques en important le paquet \(statitics\).

import statistics
moyenne    = statistics.mean(liste_num)
mediane    = statistics.median(liste_num)
ecart_type = statistics.stdev(liste_num)
print(
  '''
  Moyenne    : {}
  Mediane    : {}
  Ecart type : {}
  '''.format(moyenne,mediane,ecart_type)
)
--|  
--|    Moyenne    : 8.0
--|    Mediane    : 8.0
--|    Ecart type : 1.5811388300841898
--|  

Remarquez la présentation en colonne en utilisant une chaîne de caractères sur plusieurs lignes.

Pour les calculs numériques plus complexes, plutôt que recopier des expressions compliquées que l’on trouve sur le net en utilisation de listes et de dictionnaires, pensez à utiliser les paquets \(numpy\) (et \(sympy\) le cas échéant).


2.2.4 Numpy, tenseurs, matrices en bref

2.2.4.1 Manipulations de bases


Numpy est le “paquet” phare des calculs numériques, Tensorflow est un autre package phare dans l’univers du machine learning et de l’IA et a adopté la même syntaxe pour ses tenseurs (= tableaux).

Dans la pratique, dès que vous voulez faire des calculs numériques intenses, vous aurez à utiliser ce type de paquet. Il est donc souvent judicieux de transformer sa data numérique en tenseur après avoir extrait et nettoyé celle-ci de la base de donnée ou d’un .csv (parfois via SQL ou l’usage du package Panda).

Donc, il faut savoir transformer une liste en un tenseur Numpy (array) et vice-versa.

import numpy as np

l = [[1,2,3,4],[10,20,30,40],
     [100,200,300,400] ] # liste de listes de même dimension
print("liste de 3 listes numériques : ",l)
--|  liste de 3 listes numériques :  [[1, 2, 3, 4], [10, 20, 30, 40], [100, 200, 300, 400]]
t = np.array(l)
print(" Le tenseur correspondant : \n", t)
--|   Le tenseur correspondant : 
--|   [[  1   2   3   4]
--|   [ 10  20  30  40]
--|   [100 200 300 400]]

Il est très aisé d’accéder à une cellule du tenseur. Mais il faut se souvenir qu’en Python les indices commencent à 0, ce qui nous fait un décalage d’une position par rapport à une notation algébrique habituelle.

c = t[1,3]
print("cellule en deuxième ligne et quatrième colonne : ",c)
--|  cellule en deuxième ligne et quatrième colonne :  40

On peut également extraire une partie du tenseur par un subsetting classique sur chaque dimension. Il convient d’étudier attentivement les exemples suivants en ayant en tête que les dimensions sont séparées par des virgules et que dans une dimension l’expression \(m:n\) signifie de la ligne m+1 à la ligne n en terme mathématique et de la ligne m à la ligne n-1 en syntaxe Python où les indices commencent à zéro.

print("Première ligne : \n",                t[0   ,  : ]) 
--|  Première ligne : 
--|   [1 2 3 4]
print("Deuxième et troisième ligne  : \n ", t[1:3 ,  : ]) 
--|  Deuxième et troisième ligne  : 
--|    [[ 10  20  30  40]
--|   [100 200 300 400]]
print("Première et deuxième colonne : \n ", t[ :  , 0:2])
--|  Première et deuxième colonne : 
--|    [[  1   2]
--|   [ 10  20]
--|   [100 200]]
print("Un bloc : \n ", t[1:3 , 0:2])
--|  Un bloc : 
--|    [[ 10  20]
--|   [100 200]]

Notez qu’il est très aisé de transformer un tenseur en liste.

t.tolist() # tenseur vers liste
--|  [[1, 2, 3, 4], [10, 20, 30, 40], [100, 200, 300, 400]]

On préfère souvent déclarer en tant que matrice les structure en deux dimensions comme le tenseur précédent.

import numpy as np
l = [[1,2,3,4],[10,20,30,40],
     [100,200,300,400] ] # liste de listes de même dimension
print("liste de 3 listes numériques : ",l)
--|  liste de 3 listes numériques :  [[1, 2, 3, 4], [10, 20, 30, 40], [100, 200, 300, 400]]
M = np.matrix(l)
print("la matrice correspondante : \n", M)
--|  la matrice correspondante : 
--|   [[  1   2   3   4]
--|   [ 10  20  30  40]
--|   [100 200 300 400]]

En utilisant le type \(matrix\), vous disposez des méthodes spécifiques aux matrices. Notez que vous pouvez facilement obtenir la liste des méthodes d’un objet en utilisant la fonction \(dir()\). En règle générale, c’est une façon rapide pour explorer ce que vous pouvez faire avec un objet.

print(dir(M)[97:105]) # quelques méthodes applicables aux matrices (au hasard)
--|  ['__sub__', '__subclasshook__', '__truediv__', '__xor__', '_align', '_collapse', '_getitem', 'all']

Un tenseur et une matrice de même dimensions sont compatibles, vous pouvez effectuer des calculs triviaux avec l’un et l’autre et les combiner de façon linéaire.

print(t*100+M+0.007)
--|  [[  101.007   202.007   303.007   404.007]
--|   [ 1010.007  2020.007  3030.007  4040.007]
--|   [10100.007 20200.007 30300.007 40400.007]]

2.2.4.2 Sauvegarde et accès intelligent à un tenseur


Il est aisé de sauvegarder et lire un tenseur numpy (extension .npy).

import numpy as np

# Sauvegarde d'un array numpy
a=np.array([[0.0, 0.1, 0.2], [1.0, 1.1, 1.2]]) # deux lignes, trois colonnes
print(a, '\n de type : ', type(a))
--|  [[0.  0.1 0.2]
--|   [1.  1.1 1.2]] 
--|   de type :  <class 'numpy.ndarray'>
np.save('test_a',a)                            # le nom sera test_a.npy

a_clone = np.load('test_a.npy')                # lire le fichier
print(a == a_clone)                            # tout "match" bien
--|  [[ True  True  True]
--|   [ True  True  True]]

On peut lire partiellement un tel fichier et donc préserver la mémoire pour traiter des fichiers très volumineux.

# Lecture partielle sur disque : économie de mémoire !
a_link = np.load('test_a.npy', mmap_mode='r') # lien sur le fichier
print(a == a_link)                            # tout "match" bien  
--|  [[ True  True  True]
--|   [ True  True  True]]
print(a_link, '\n de type : ', type(a_link))  # ce n'est pas un vrai array !
--|  [[0.  0.1 0.2]
--|   [1.  1.1 1.2]] 
--|   de type :  <class 'numpy.memmap'>
print(a_link[ :, 1]) # accés direct sur disque à la deuxième colonne i.e. col 1
--|  [0.1 1.1]

On peut créer des fichiers comprimés, contenant plusieurs tenseurs ayant chacun un identifiant unique (extension .npz).

# Sauvegarde compressée de plusieurs structures et accès successifs
b=np.array([[0.0, 0.1], [1.0, 1.1]]) # deux lignes, deux colonnes

np.savez('test_a_b',                 # sauvegarde compr aves clés = identifiants
         a=a, 
         b=b)

a_b = np.load('test_a_b.npz')
a_clone = a_b.get('a')               # accès aux tableaux par leur identifiant
b_clone = a_b.get('b')

Voici une façon de dépiler l’ensemble des tenseurs contenus dans le fichier.

# Accès naïf à l'ensemble des valeurs de chaque tableau
k = list(a_b)        # liste des clés
for i in k : 
  t =  a_b.get(i)
  print("\nL'array {} vaut : \n {} ".format(i,t))
--|  
--|  L'array a vaut : 
--|   [[0.  0.1 0.2]
--|   [1.  1.1 1.2]] 
--|  
--|  L'array b vaut : 
--|   [[0.  0.1]
--|   [1.  1.1]]

Ces techniques sont importantes à identifier dans le cadre de la Business Intelligence, du Big Data ou du Machine Learning.

2.3 Les dictionnaires Python

2.3.1 Introduction aux dictionnaires

Cette structure est très appréciées par les développeurs Python, trop peut-être. Avant de l’utiliser, vérifiez toujours si une liste, un tableau Numpy ou un dataframe Pandas ne serait pas plus simple, plus puissant ou plus adapté. Globalement un dictionnaire est une liste de couples clé - valeur, on peut imaginer les clés comme des index. Contrairement aux listes, aux tableaux et au t-uplets, l’ordre de rangement de ces informations dans le dictionnaire ne devrait pas vous importer puisque l’on accède à chaque valeur au travers de sa clé.

Les dictionnaires sont là pour vous simplifier la vie ! Quand vous devez faire appel à des techniques compliquées ou difficilement lisibles pour utiliser vos dictionnaires, c’est sans doute que cet outil n’est pas le plus judicieux. Vous pouvez toujours transcrire vos données dans un autre structure plus adaptée pour gagner en fiabilité.

Les listes et le subsetting se caractérisent syntaxiquement par l’usage des crochets \([]\), les dictionnaires se caractérisent par l’usage des accolades \(\{ \}\).

La création d’un petit dictionnaire est très aisée. En terme de clé (key en anglais), vous trouverez le plus souvent des chaînes de caractère ou des entiers. En terme de valeur, c’est comme pour les listes, vous pouvez stocker tout les types d’objet.

Voici un exemple de dictionnaire et d’usage de la méthode \(get\) avec le traitement du cas où la clé n’existe pas. Remarquez que dans toute requête il faut toujours traiter le cas où elle est infructueuse, même si vous imaginez ce cas fonctionnellement impossible !

sigles = {
  "CCNE"  : "Comité consultatif national d'éthique",
  "ADEME" : "Agence de l'environnement et de la maîtrise de l'énergie",
  "YAML"  : "Ain t markup language",
  "CEE"   : "Communauté économique européenne"
}

def trouve_sigle(s) : return sigles.get(s,"sigle inconnu") 

print(trouve_sigle( "CCNE"))
--|  Comité consultatif national d'éthique
print(trouve_sigle( "TOTO"))
--|  sigle inconnu

On peut facilement lister les t-uplets qui composent le dictionnaire.

for s in sigles.items() : print(s)
--|  ('CCNE', "Comité consultatif national d'éthique")
--|  ('ADEME', "Agence de l'environnement et de la maîtrise de l'énergie")
--|  ('YAML', 'Ain t markup language')
--|  ('CEE', 'Communauté économique européenne')
print("\nclasse de l'objet s : ", s.__class__) # vérification de la classe de s
--|  
--|  classe de l'objet s :  <class 'tuple'>

La liste des clés est également facile à obtenir.

for key in sigles.keys() : print(key)
--|  CCNE
--|  ADEME
--|  YAML
--|  CEE

2.3.2 Se doter d’une fonction print pour les structures complexes via pprint

Les dictionnaires, ou les objet JSON, sont des structures un peu difficiles à imprimer proprement. Pour ce faire nous vous proposons d’utiliser le package pprint (pour pretty print). L’idée est de créer une ou plusieurs interfaces d’impression paramétrées pour les objets spécifiques que vous voulez imprimer.

import pprint
pp_dict = pprint.PrettyPrinter(indent=3, width=80, compact=True)
pp_dict.pprint(sigles)
--|  {  'ADEME': "Agence de l'environnement et de la maîtrise de l'énergie",
--|     'CCNE': "Comité consultatif national d'éthique",
--|     'CEE': 'Communauté économique européenne',
--|     'YAML': 'Ain t markup language'}

2.3.3 Manipulation des disctionnaires

On peut affecter ou réaffecter directement une valeur sur une clé connue :

sigles["CEE"] = 'Communauté Economique Européenne'
pp_dict.pprint(sigles)
--|  {  'ADEME': "Agence de l'environnement et de la maîtrise de l'énergie",
--|     'CCNE': "Comité consultatif national d'éthique",
--|     'CEE': 'Communauté Economique Européenne',
--|     'YAML': 'Ain t markup language'}

De façon “étrange” on peut aussi créer une nouvelle entrée de dictionnaire par une affectation avec le signe égal :

sigles["XXX"] = 'Xana Xin Xug'
pp_dict.pprint(sigles)
--|  {  'ADEME': "Agence de l'environnement et de la maîtrise de l'énergie",
--|     'CCNE': "Comité consultatif national d'éthique",
--|     'CEE': 'Communauté Economique Européenne',
--|     'XXX': 'Xana Xin Xug',
--|     'YAML': 'Ain t markup language'}

On peu supprimer une entrée et l’extraire :

sigles_extrait = sigles.pop("XXX") 
print("ce sigle n'est plus dans la liste : ",sigles_extrait)
--|  ce sigle n'est plus dans la liste :  Xana Xin Xug
pp_dict.pprint(sigles)
--|  {  'ADEME': "Agence de l'environnement et de la maîtrise de l'énergie",
--|     'CCNE': "Comité consultatif national d'éthique",
--|     'CEE': 'Communauté Economique Européenne',
--|     'YAML': 'Ain t markup language'}

Essayons maintenant un code qui tiendrait compte du fait que la clé existe ou pas, ici la clé existe :

sigles["XXX"] = 'Xana Xin Xug'

x_ = "XXX" 
if   x_ in sigles :                          # trouve_sigle(x_)!= "sigle inconnu" : 
     s_ = sigles.pop("XXX") 
     print("le sigle {} de signification {} n'est plus dans la liste".format(x_,s_))
else :
     print("le sigle {} n'était pas dans la liste".format(x_))
--|  le sigle XXX de signification Xana Xin Xug n'est plus dans la liste

Maintenant la clé n’existe plus :

x_ = "XXX" 
if   x_ in sigles : 
     s_ = sigles.pop("XXX") 
     print("le sigle {} de signification {} n'est plus dans la liste".format(x_,s_))
else :
     print("le sigle {} n'était pas dans la liste".format(x_))
--|  le sigle XXX n'était pas dans la liste

Il existe une syntaxe typique des dictionnaires, à étudier attentivement, à savoir le fait de parcourir le dictionnaire des items du dictionnaire en extrayant à la fois la clé et la valeur correspondante. Dans la documentation Python, souvent on ne s’ embarrasse pas à nommer la clé \(key\) et la valeur \(value\), mais on se contente dire \(k,v\).

for key,value  in sigles.items() : print(key," :" , value)
--|  CCNE  : Comité consultatif national d'éthique
--|  ADEME  : Agence de l'environnement et de la maîtrise de l'énergie
--|  YAML  : Ain t markup language
--|  CEE  : Communauté Economique Européenne

Evidemment, comme on dispose des clés, on peut manipuler les résultats à notre convenance.

for k,v  in sigles.items() : print(v.upper()," :" , k)
--|  COMITÉ CONSULTATIF NATIONAL D'ÉTHIQUE  : CCNE
--|  AGENCE DE L'ENVIRONNEMENT ET DE LA MAÎTRISE DE L'ÉNERGIE  : ADEME
--|  AIN T MARKUP LANGUAGE  : YAML
--|  COMMUNAUTÉ ECONOMIQUE EUROPÉENNE  : CEE

Pour des besoins de présentation, ou avant d’extraire une liste de votre dictionnaire, afin de créer une structure de nature différente vous pouvez décider de trier celui-ci. *Attention, \(sorted\) génère une liste de t_uplet et donc il faut la tansformer en dictionnaire via la fonction \(dict\).

sigles_t_k = dict(sorted(sigles.items()))

for k,v in sigles_t_k.items() : print(k,v)
--|  ADEME Agence de l'environnement et de la maîtrise de l'énergie
--|  CCNE Comité consultatif national d'éthique
--|  CEE Communauté Economique Européenne
--|  YAML Ain t markup language

Pour trier par valeur, il faut stipuler que la clé de tri (rien à voir avec la clé du dictionnaire) attendue par la fonction \(sorted\) porte sur la deuxième colonne de l’objet à trier (donc \([1]\). Pour ce faire on utilise une fonction anonyme (via l’instruction \(lambda\) ). On déclare que la clé de tri (\(key\)) sera le résultat de l’application de la fonction sur l’objet à trier (\(x\)), fonction qui pour ce qui est de notre dictionnaire que nous voulons trier sur les valeurs,retournera donc la deuxième colonne (\(x[1]\)).

Ce type de syntaxe, avec l’usage de \(lambda\) est très courant et utile en Python, vous devez faire l’effort d’en assimiler le sens de façon non superficielle ! D’autant plus que cela vous permettra de fiabiliser votre compréhension des concepts de la programmation fonctionnelle.

sigles_t_v = dict(sorted(sigles.items(), 
                         key=lambda x: x[1])) # colonne [1, donc tri par valeur

for k,v in sigles_t_v.items() : print(v," :", k)
--|  Agence de l'environnement et de la maîtrise de l'énergie  : ADEME
--|  Ain t markup language  : YAML
--|  Comité consultatif national d'éthique  : CCNE
--|  Communauté Economique Européenne  : CEE

L’aide de fonction \(sorted\) vous donne les informations suivantes :

sorted(iterable, /, *, key=None, reverse=False)
Return a new list containing all items from the iterable in ascending order.

A custom key function can be supplied to customize the sort order, and the
reverse flag can be set to request the result in descending order.

Vous pouvez y lire que la fonction retourne une \(list\), qu’elle fonctionne sur toutes les structures \(iterable\), c’est à dire où l’on dispose d’un “itérateur” qui permet de passer à l’élément suivant dans un ordre précis : “au suivant”(\(next\)). On constate aussi que l’on peut déclarer une fonction pour définir la clé de tri (\(key =\)), comme nous l’avons fait via \(key=lambda \space x: x[1]\). Evidemment la fonction dépend de type d’objet que l’on est en train de trier. Par ailleurs, on remarque que l’on peut inverser l’ordre de tri via \(reverse=True\).

2.4 Zoom sur les générateurs

Pour créer son propre “itérateur”, il est commode d’utiliser un générateur (d’itérateurs). C’est une fonction spécifique qui, au lieu de retourner une valeur via un \(return\), retourne la prochaine valeur à utiliser lors du parcours d’une structure itérable. L’instruction permettant de spécifier cette valeur est \(yield\), elle fonctionne comme \(return\).

Mettez le générateur suivant dans votre boîte à outils. A titre d’exercice, tentez de le modifier et analysez les conséquences de vos modifications.

def gen_i(start,end):
    ''' gen_i génère un intérateur d'incrément de start à end inclus '''
    for i in range(start - 1,end):
        i = i + 1
        yield i

Nous allons l’utiliser dans une boucle \(for\), ce qui est très souvent le destin d’un itérateur !

for j in gen_i(-2,3) : print(j) # une boucle sur j variant de -2 à 3
--|  -2
--|  -1
--|  0
--|  1
--|  2
--|  3

C’est affligeant de simplicité !

def gen_i_pair(start, end):
    ''' générateur de nombres pairs ''' 
    for i in range(start - 1, end):
        i = i+1
        if i.__mod__(2)== 0 : yield i # si est divisible par 2
                                      # pour impair, on aurait ==1
            
for j in gen_i_pair(-4, 9) : print(j) # nombres pairs de -4 à 8 inclus
--|  -4
--|  -2
--|  0
--|  2
--|  4
--|  6
--|  8

Ce générateur pourrait nous permettre de faire des calculs sur la liste des n premiers entiers pairs : “pour tous les entiers pairs de 0 à n : faire f(j)”.

Reprenons notre dictionnaire de sigles et voyons comment créer un générateur de tous les sigles commençant par une lettre donnée.

def g_sigles_X(X) :
    for key in sigles.keys() :
        if key[0] == X : yield key

Nous pouvons maintenant faire une action concernant les clés de \(sigles\) commençant par une lette quelconque, ici “C”.

for X in g_sigles_X("C") : print(X)
--|  CCNE
--|  CEE

Ce mécanisme est très résilient, en effet, dans notre cas, si vous demandez la même action alors que la condition ne peut pas être remplie, il ne se passe rien.

for X in g_sigles_X("Z") : print(X) # n'imprime rien car pas de sigle avec Z

2.5 Créer ses propres objets

Pour créer un objet (une instance de classe), il vous faut une classe afin de pouvoir déclarer que cet objet hérite de cette classe. Ici nous créons une classe \(citoyen\) et via l’instruction \(individu = Citoyen() ...\) nous implémentons la définition : “cet individu is_a citoyen”.

La classe est décrite au travers de ses méthodes. La première méthode à construire est le constructeur de la classe, en Python init. C’est cette méthode qui est évoquée lorsque l’on crée un instance de la classe.

Le premier paramètre de votre classe se nomme \(self\) et permet de préfixer les variables internes de la future instance en faisant référence à l’objet lui-même.

Le premier travail du constructeur, c’est de renseigner les variables internes \(self.xxx = yyy\). Dans les cas simples comme ici, on se contente d’affecter aux variables internes les valeurs entrées dans les paramètres du constructeur, comme ceci : \(self.nom = nom\). Si nécessaire, on peut effectuer des initialisations beaucoup plus riches et procéder à diverses vérifications de cohérence.

Après avoir exprimé le constructeur, on ajoute toutes les méthodes qui définiront le comportement de la classe.

class Citoyen :
  ''' Classe décrivant les attributs minimum d'un Français identifié '''
  
  def __init__(self, nom, prenom, num_ss) :
      self.nom    = nom
      self.prenom = prenom
      self.num_ss = num_ss
    
  def genre(self) : # dans le num SS le genre est à la première position
      ''' retourne le genre de naissance d'un citoyen ''' 
      return "féminin" if self.num_ss[0] == "2" else "masculin"
  

On peut donc maintenant instancier un objet de la classe \(Citoyen\).

individu_1 = Citoyen("Martin","Pierre","1891195555494")

Les variables et les méthodes issues de la classe citoyen sont maintenant accessibles pour l’\(individu_1\).

print("Le genre de {} {} est {}"
      .format(individu_1.prenom, individu_1.nom, individu_1.genre()))
--|  Le genre de Pierre Martin est masculin

Excepté pour les projets d’analyse de données massives ou vous manipules des lignes reprèsentant des observations, vous devez envisager la programmation orientée objet dès que vous voyez apparaître des agrégats logiques de caractéristiques et de comportement dans votre analyse. L’analyse de votre problème via UML favorise l’émergence de classes et suggère leurs méthodes. Quand vous programmez la modification ou la saisie d’une structure via une interface homme/machine, il est judicieux d’associer vos objets fonctionnels aux objets graphiques de niveau hiérarchique élevé. Par exemple l’objet individu de la classe Citoyen sera modifié au travers de l’objet instance d’écran de saisie Citoyen. L’objet individu sera lui-même lu et écrit (on dit “sérialisé”) et validé par ses méthodes via des méthodes qui invoqueront les requêtes en base de donnée (par exemple en SQL).

Dans le cas où vous avez besoin de créer des objets simples non modifiables (en anglais : immutable) et sans méthode ni contrôles spécifique, nous vous conseillons une écriture rapide et efficace qui consiste à utiliser la classe \(namedtuple\) du paquet \(collections\).

from collections import namedtuple
 
# Création d'une CLASSE Fiche_citoyen
Fiche_citoyen = namedtuple('Fiche_citoyen', 'nom prenom num_ss') 
 

A partir de notre nouvelle classe \(Fiche\_citoyen\) on peut instancier un nouvel individu.

individu_2 = Fiche_citoyen("Dupond", "Marie","2891195555494" )
print(individu_2.__repr__()) # __repr__ retourne une visualisation agréable 
--|  Fiche_citoyen(nom='Dupond', prenom='Marie', num_ss='2891195555494')

On peut accéder aux variables internes de la classe en utilisant la technique habituelle.

print(individu_2.num_ss)
--|  2891195555494