1. Python - Le Langage



1.1. Historique

Le langage Python a été créé à partir de 1980 par Guido van Rossum et le nom Python fait référence au Monty Python's Flying Circus.

La première version (Python 0.9.0) date de Février 1991 et il a évolué jusqu'à la version 3 (Décembre 2008) pour gagner en maturité. Il est souvent enseigné comme premier langage de programmation, grâce à sa lisibilité et sa simplicité malgré quelques défauts liés à sa syntaxe et sa lenteur d'interprétation.

Python doit son succès :

Voici rapidement quelques spécificités du langage :

  • le langage est interprété
  • le typage est dynamique
  • la casse est discriminatoire : on distingue majuscules et minuscules ce qui implique que x et X sont deux choses différentes
  • on utilise les tabulations (ou les espaces) pour indenter le code et faire apparaître sa structure

1.2. Documentation

1.3. Editeur

L'indentation en Python est primordiale car elle fait apparaître la structure du code mais est également utilisée pour savoir à quel bloc une instruction appartient.

Certain éditeurs remplacent les tabulations par des espaces ce qui peut amener Python à générer des erreurs de syntaxe.

Il est donc conseillé d'utiliser toujours le même environnement de développement (IDE). Parmi les IDE les plus populaires pour Python, on trouve :

1.4. Caractéristiques du langage

1.4.1. langage interprété

On distingue les langages interprétés (PHP, Python, Javascript) des langages compilés (C, C++, Fortran, Pascal).

Pour un langage compilé, un programme appelé compilateur transforme le code en instructions exécutables par le microprocesseur de l'ordinateur.

Pour un langage interprété, un programme appelé interpréteur lit une instruction, l'exécute, puis passe à la suivante.

Le constat est qu'un langage compilé génère du code qui s'exécute beaucoup plus rapidement qu'un langage interprété (cf fractales de Mandelbrot, facteur d'amélioration de 420 / 6 = 70).

On peut, pour mieux appréhender ce concept, utiliser une analogie : imaginons que vous ne parliez que français et que vous soyez amené à consulter un livre en russe. Deux possibilités s'offrent à vous :

1.4.2. typage dynamique

En informatique, on distingue le typage dynamique du typage statique.

Le typage consiste à attribuer un type (booléen, entier, chaîne, réel, ...) à une variable afin de savoir comment traiter la variable.

1 + 2 => 3		int + int -> int

"bonjour " + "Jean-Michel" => "bonjour Jean-Michel" (str + str -> str, concaténation)

1 + "bonjour" => ?
TypeError: unsupported operand type(s) for +: 'int' and 'str'

"bonjour" + 1 =?> "bonjour1"
TypeError: can only concatenate str (not "int") to str

"bonjour" + str(1) => "bonjour1"
  • Le typage statique attribue un seul type à une variable pour la durée de son existence.
  • Le typage dynamique consiste à modifier le type d'une variable lorsqu'on lui affecte une valeur.
x = 1 (entier, int)
y = 3.14 (réel, float)
s = "bonjour" (chaine, str)

Notons que le typage dynamique est une notion qui est associée aux langages interprétés.

Par exemple en Python :

>>> x = 10
>>> type(x)
<class 'int'>
>>> id(x)	(adresse de x en mémoire)
11753896
>>> x = 3.14
>>> type(x)
<class 'float'>
>>> x = "coucou"
>>> type(x)
<class 'str'>
>>> x = [1,2,3]
>>> type(x)
<class 'list'>
>>> id(x)
123902002645824

1.5. Avantages et inconvénients

1.5.1. Avantages

1.5.2. inconvénients

1.6. Les types de données

Nous allons nous intéresser principalement aux types de données suivants :

Il existe également d'autres types que nous ne verrons pas dans ce cours : complex, function, frozenset, bytes, bytearray, memoryview, ...

1.6.1. Conversion de type

Il suffit généralement de préfixer la valeur avec le nom du type :

# chaine vers entier
entier = int("125")

# entier vers réel
reel = float(123)

# réel vers entier => 3
entier = int(3.14)

# ensemble vers liste
liste = list({1,2,3})

# liste vers ensemble => { 1,2,3 }
ensemble = set([1, 2, 3, 3, 3, 2, 1, 3])

1.6.2. Booléens (bool)

Les booléens sont des valeurs représentant la vérité : True ou False.

# Exemple
est_vrai = True
print(type(est_vrai))  # Résultat : <class 'bool'>

1.6.3. Entiers (int)

Les nombres entiers représentent des valeurs numériques sans partie décimale.

# Exemple
nombre = 42
print(type(nombre))  # Résultat : <class 'int'>

1.6.4. Range ou intervalle d'entiers

range(b) représente l'intervalle d'entiers $[0,b[$ de $0$ à $b-1$

range(a,b) représente l'intervalle d'entiers $[a,b[$, de $a$ à $b-1$

range(a,b,c) représente l'intervalle d'entiers $[a,b[$ tel que les nombres qu'il représente sont $a + i × c$ jusqu'à $b-1$

range est utilisé avec les boucles for.

# Exemple
r = range(10)
print(list(r)) # Résultat : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

r = range(1,10)
print(list(r))  # Résultat : [1, 2, 3, 4, 5, 6, 7, 8, 9], <class 'range'>

r = range(4,1,-1)
print(list(r))  # Résultat : [4,3,2]

1.6.5. Nombres flottants (float)

Les flottants représentent des nombres réels (cf. flottants).

# Exemple
nombre_decimal = 3.14
epsilon = 8.91e-7
print(type(nombre_decimal))  # Résultat : <class 'float'>

Dans la version 3 de Python, on utilise la représentation IEEE 754 pour les nombres en virgule flottante en double précision :

  • valeurs maximales : $±1.7976931348623157×10^308$ (sys.float_info.max)
  • valeurs minimales : $±2.2250738585072014×10^{−308}$ (sys.float_info.min)
  • précision de 15 à 17 chiffres significatifs

1.6.6. Chaînes de caractères (str)

Les chaînes sont utilisées pour représenter du texte.

# Exemple
texte = "Bonjour, Python !"
print(type(texte))  # Résultat : <class 'str'>

Les chaînes sur plusieurs lignes sont introduites par des triples guillemets simples ou doubles :

# Exemple
une_longue_chaine = '''Bonjour, Python !
voici une chaine sur 
plusieurs lignes'''
print(type(une_longue_chaine))  # Résultat : <class 'str'>

Un caractère est une chaîne composée d'un seul caractère ou de longueur 1.

On obtient la longueur d'une chaîne grâce à la fonction len().

Il existe deux fonctions importantes pour les caractères :

  • ord("c") donne la valeur ASCII/UTF-8 du caractère
  • chr(int) transforme l'entier en un caractère
ord("A")
65
ord("€")
8364 
print(chr(65))
A
print(chr(8364))
€

Le package string contient un nombre important de fonctions qui permettent de gérer les chaînes de caractères.

Le type string dispose également de nombreuses fonctions (cf lien) : split, strip, upper, lower, swapcase, startswith, join, isalpha, isdigit, ...

Enfin, il existe deux autres types de chaînes de caractères :

  • les b-strings (ou chaînes d'octets) sont créées en ajoutant un préfixe b avant les guillemets : b"...". Elles contiennent une séquence brute d'octets (valeurs entre 0 et 255) qui ne sont pas directement compatible avec les chaînes unicode.
  • les f-strings (formatted - introduites dans Python 3.6) permettent d'insérer des expressions Python directement dans une chaîne en utilisant des accolades {}. Elles sont créées avec un préfixe f ou F.
x = 1
pi = 3.14
print(f"{x} {pi/60} {f(3)}") # Résultat 1 0.052333333333333336 9

1.6.7. Listes (list)

Les listes sont des collections ordonnées et modifiables. On peut aussi les qualifier de tableaux car on peut en accéder les éléments par leur indice entier.

Une liste peut contenir des éléments de types différents.

# Exemple
ma_liste = [1, 2, 3, "Python", True]
print(type(ma_liste))  # Résultat : <class 'list'>
print(ma_liste[3]) # Résultat : Python

On obtient la longueur d'une liste grâce à la fonction len().

# Exemple
ma_liste = [1, 2, 3, "Python", True]
print(len(ma_liste))  # Résultat 5

1.6.7.a  vérification de la présence d'un élément dans la liste

On utilise in pour vérifier la présence de l'élément ou not in pour vérifier l'absence de l'élément.

  1. l = [1, 3, 4, 7]
  2. if 3 in l:
  3.     print("présence")
  4. if 5 not in l:
  5.     print("absence")
  6.  
présence
absence

1.6.7.b  ajout d'un élément en début de liste

On utilise soit la méthode insert(indice,élément), soit la concaténation de listes :

  1. liste = [ "peugeot", "renault"]
  2.  
  3. # utilisation de la méthode insert
  4. liste.insert(0, "fiat")
  5. print(liste)
  6.  
  7. # utilisation de la concaténation de listes
  8. liste = ["audi"] + liste
  9. print(liste)
  10.  
  11.  
['fiat', 'peugeot', 'renault']
['audi', 'fiat', 'peugeot', 'renault']

1.6.7.c  ajout d'un élément en fin de liste

On utilise soit la méthode append(élément), soit la concaténation de listes :

  1. liste = [ "peugeot", "renault"]
  2.  
  3. # utilisation de append()
  4. liste.append("toyota")
  5. print(liste)
  6.  
  7. # utilisation de la concaténation de liste
  8. liste = liste + ["volkswagen"]
  9. print(liste)
  10.  
['peugeot', 'renault', 'toyota']
['peugeot', 'renault', 'toyota', 'volkswagen']

1.6.7.d  ajout d'un élément dans la liste

On utilise soit la méthode insert(indice,élément), soit la concaténation de listes :

  1. liste = [ "audi", "fiat", "peugeot", "renault"]
  2. liste.insert(1, "citroën")
  3. print(liste)
  4.  
  5. liste = [ "audi", "fiat", "peugeot", "renault"]
  6. liste = liste[:2] + ["hyundai"] + liste[-2:]
  7. print(liste)
  8.  
['audi', 'citroën', 'fiat', 'peugeot', 'renault']
['audi', 'fiat', 'hyundai', 'peugeot', 'renault']

1.6.7.e  suppression d'un élément en début de liste

On utilise soit l'instruction del, soit la méthode pop(0) :

  1. liste = [ "audi", "fiat", "peugeot", "renault"]
  2. del liste[0]
  3. print(liste)
  4.  
  5. liste.pop(0)
  6. print(liste)
  7.  
['fiat', 'peugeot', 'renault']
['peugeot', 'renault']

1.6.7.f  suppression d'un élément en fin de liste

On utilise soit l'instruction del, soit la méthode pop() (sans argument) :

  1. liste = [ "audi", "fiat", "peugeot", "renault"]
  2. del liste[-1]
  3. print(liste)
  4.  
  5. liste.pop()
  6. print(liste)
  7.  
['audi', 'fiat', 'peugeot']
['audi', 'fiat']

1.6.7.g  suppression d'un élément dans la liste

On utilise soit l'instruction del, soit la méthode pop(n) (où $n$ est l'indice de l'élément), soit la méthode remove(élément) :

  1. liste = [ "audi", "fiat", "peugeot", "renault", "volkswagen" ]
  2.  
  3. # supprimer l'élément suivant sa valeur
  4. liste.remove("peugeot")
  5. print(liste)
  6.  
  7. # supprimer l'élément suivant son indice avec del
  8. del liste[2]
  9. print(liste)
  10.  
  11. # supprimer l'élément suivant son indice avec pop
  12. liste.pop(1)
  13. print(liste)
  14.  
  15.  
['audi', 'fiat', 'renault', 'volkswagen']
['audi', 'fiat', 'volkswagen']
['audi', 'volkswagen']

1.6.7.h  Création de listes par compréhension

La création de listes par compréhension en Python (ou list comprehension) est une syntaxe concise et élégante (qui s'apparente à la syntaxe du langage naturel) permettant de générer des listes à partir d'itérables, tout en appliquant des transformations ou des filtres. Elle remplace souvent une boucle for classique pour rendre le code plus lisible et plus compact.

La syntaxe est la suivante :

[ nouvel_élément for élément in iterable if condition]

La partie if condition est optionnelle.

Par exemple on peut écrire :

liste = [x for x in range(5)]
print(liste)  # [0, 1, 2, 3, 4]

au lieu de :

liste = []
for x in range(5):
	liste.append(x)

Voici un exemple avec deux for :

combinaisons = [(x, y) for x in range(2) for y in range(2)]
print(combinaisons)  # [(0, 0), (0, 1), (1, 0), (1, 1)]

Afin de créer une liste d'éléments constants, on peut écrire :

liste_de_zeros = [0 for _ in range(5)]
print(liste_de_zeros)  # Résultat : [0, 0, 0, 0, 0]

Voici un autre exemple avec un for et un if :

pairs = [x for x in range(10) if x % 2 == 0]
print(pairs)  # [0, 2, 4, 6, 8]

1.6.8. Tuples (tuple)

Les tuples sont des collections ordonnées mais immuables, ce sont des listes non modifiables. Elles sont introduites par (...).

# Exemple
mon_tuple = (1, 2, 3, "Python")
print(type(mon_tuple))  # Résultat : <class 'tuple'>
print(t[0]) # Résultat : 1
print(t[3]) # Résultat : Python

1.6.9. Dictionnaires (dict)

Les dictionnaires stockent des paires clé-valeur : à une clé est associée une valeur. Ils sont introduits par {...}.

# Exemple
mon_dico = {"nom": "Alice", "âge": 30}
print(type(mon_dico))  # Résultat : <class 'dict'>
print(mon_dico["nom"]) # Résultat : "Alice"
  • on peut accéder les clés grâce à .keys()
  • on peut accéder les valeurs grâce à .values()
  • on peut accéder les (clés,valeurs) grâce à .items()
mon_dico = { 1: "rouge", 2:"vert", 3:"bleu", 0:"noir" }
print(mon_dico.values())
# Résultat : dict_values(['rouge', 'vert', 'bleu', 'noir'])
print(mon_dico.keys())
# Résultat : dict_keys([1, 2, 3, 0])
print(mon_dico.items())
# Résultat : dict_items([(1, 'rouge'), (2, 'vert'), (3, 'bleu'), (0, 'noir')])

1.6.9.a  vérification de la présence d'un élément dans un dictionnaire

On utilise in pour vérifier la présence et not in pour vérifier l'absence :

  1. dico = { 3: "bleu", 0 :  "noir", 2 : "vert", 1: "rouge" }
  2.  
  3. cle = 3
  4. if cle in dico:
  5.     print(dico[cle])
  6. cle = 7
  7. if cle not in dico:
  8.     print("clé absente")
  9.  
bleu
clé absente

1.6.9.b  ajout d'un élément dans un dictionnaire

On utilise dict[clé]=valeur ou la méthode update :

  1. dico = { 3: "bleu" }
  2.  
  3. dico.update({ 0 :  "noir", 2 : "vert" })
  4. print(dico)
  5.  
  6. dico[1] = "rouge"
  7. print(dico)
{3: 'bleu', 0: 'noir', 2: 'vert'}
{3: 'bleu', 0: 'noir', 2: 'vert', 1: 'rouge'}

1.6.9.c  suppression d'un élément dans un dictionnaire

On utilise del ou la méthode pop() qui retourne la valeur associée à la clé :

  1. dico = { 3: "bleu", 0 :  "noir", 2 : "vert", 1: "rouge" }
  2.  
  3. del dico[3]
  4. print(dico)
  5.  
  6. valeur = dico.pop(1)
  7. print(valeur)
  8. print(dico)
  9.  
{0: 'noir', 2: 'vert', 1: 'rouge'}
rouge
{0: 'noir', 2: 'vert'}

1.6.10. Ensembles (set)

Les ensembles sont des collections non ordonnées de valeurs uniques.

# Exemple
mon_ensemble = {1, 2, 3, 3, 2, 1, 3}
print(mon_ensemble)  # Résultat : {1, 2, 3}
print(type(mon_ensemble))  # Résultat : <class 'set'> 

print(mon_ensemble[0]) # Résultat : 1

Les éléments d'un ensemble ne sont pas manipulable grâce à l'opérateur [], il faut transformer l'ensemble en liste ou alors parcourir les éléments de l'ensemble grâce à une boucle for.

1.6.10.a  vérification de la présence d'un élément dans un ensemble

On utilise in pour vérifier la présence et not in pour vérifier l'absence :

  1. ensemble = { 1, 3, 5}
  2.  
  3. if 3 in ensemble:
  4.     print("3 est présent")
  5. else:
  6.     print("3 n'est pas présent")
  7.  
  8. if 4 not in ensemble:
  9.     print("4 n'est pas présent")
  10. else:
  11.     print("4 est présent")
  12.  
3 est présent
4 n'est pas présent

1.6.10.b  ajout d'un élément dans un ensemble

On utilise :

  • soit la méthode add(élement)
  • soit la méthode update({élements})
  1. ensemble = { 1, 2, 3}
  2.  
  3. ensemble.add(4)
  4. print(ensemble)
  5.  
  6. ensemble.update({1, 3, 4, 5, 6})
  7. print(ensemble)
{{1, 2, 3, 4}
{1, 2, 3, 4, 5, 6}

1.6.10.c  suppression d'un élément dans un ensemble

On utilise :

  • soit la méthode remove(élement)
  • soit la méthode discard(élement) qui ne lève pas d'erreur si l'élément n'est pas présent
  • soit la méthode pop() qui supprime un élément quelconque
  1. mon_ensemble = set([ x for x in range(0,16) if x % 2 != 0])
  2. print(mon_ensemble)
  3.  
  4. if 3 in mon_ensemble:
  5.     print("3 est dans l'ensemble")
  6.  
  7. mon_ensemble.remove(3)
  8.  
  9. if 3 not in mon_ensemble:
  10.     print("3 n'est pas dans l'ensemble")
  11.  
  12. mon_ensemble.discard(3)
  13.  
  14. element = mon_ensemble.pop()
  15. print(element)
  16.  
  17. for i, x in enumerate(mon_ensemble):
  18.     print(f"{i:2} => {x}")
{1, 3, 5, 7, 9, 11, 13, 15}
3 est dans l'ensemble
3 n'est pas dans l'ensemble
1
 0 => 5
 1 => 7
 2 => 9
 3 => 11
 4 => 13
 5 => 15

1.6.11. Les fichiers

Les fichier permettent de sauvegarder de l'information sur le disque (HDD/SSD).

On utilise plusieurs notions pour la lecture ou l'écriture des fichiers. notamment :

  • with open(nom_fichier, mode) as variable
  • les exceptions pour intercepter les erreurs éventuelles (fichier absent, permissions)

Le paramètre mode peut prendre plusieurs valeurs ou la combinaison de ces valeurs :

  • b : fichier binaire
  • t : fichier texte (par défaut)
  • r : ouverture en lecture
  • w : ouverture en écriture avec remise à zéro du fichier
  • a : ouverture en écriture avec ajout en fin du fichier (append)

1.6.11.a  Ecriture dans un fichier texte

On utilise la fonction write :

  1. chaine = '''Bonjour le monde !
  2. lorem ipsum dolor
  3. '''
  4.  
  5. try:
  6.     # ouverture d'un flux en écriture 'w'
  7.     with open("texte.txt", "w") as fichier:
  8.         fichier.write(chaine)
  9.         print(type(fichier))
  10.         fichier.close()
  11.  
  12. # gestion des exceptions liées à 'try:'
  13. except FileNotFoundError:
  14.     print("fichier non existant")
  15. except PermissionError as e2:
  16.     print(f"{e2}")
  17. except Exception as e3:
  18.     print(f"Une erreur est survenue : {e3}")
<class '_io.TextIOWrapper'>

1.6.11.b  Lecture d'un fichier texte

On utilise read() pour lire la totalité du fichier sous forme d'une chaîne ou readlines() qui retourne une liste de lignes.

  1.  
  2. try:
  3.     nom_fichier = "texte.txt"
  4.     # ouverture d'un flux en lecture 'r'
  5.     with open(nom_fichier, 'r', encoding='utf-8') as fichier:
  6.         # lecture du fichier texte comme une seule chaine
  7.         #texte = fichier.read()
  8.         # ou lecture comme plusieurs lignes
  9.         texte = fichier.readlines()
  10.         print(texte)
  11.         print(type(fichier))
  12.         fichier.close()
  13.  
  14. # gestion des exceptions liées à 'try:'
  15. except FileNotFoundError as e1:
  16.     print(f"{e1}")
  17.     print(f"fichier non trouvé {nom_fichier}")
  18. except Exception as e2:
  19.     print(f"Une erreur est survenue : {e2}")
  20.  
['Bonjour le monde !\n', 'lorem ipsum dolor\n']
<class '_io.TextIOWrapper'>

1.7. Les instructions

1.7.1. L'instruction if-elif-else

Le if appelée conditionnelle permet de réaliser un traitement si une condition est vraie :

  1. a = 2
  2. b = 0
  3.  
  4. if a > 0 and b < 10:
  5.     print("valeurs valides")
  6. elif b == 0:
  7.     print("division par 0 impossible")
  8. else:
  9.     print("valeurs non valides")
  10.  
  11.  
valeurs valides

1.7.2. L'instruction match

Le match (encore appelé switch ou case of dans d'autres langage) et l'équivalent d'un if mais ne s'applique la plupart du temps qu'à des valeurs scalaires comme les entiers :

  1. a = 2
  2.  
  3. match a:
  4.     case 1:
  5.         print("a=1")
  6.  
  7.     case 2:
  8.         print("a=2")
  9.  
  10.     case 3:
  11.         print("a=3")    
  12.  
  13.     case _:
  14.         print("valeur non valide")
  15.  
  16.  
a=2

1.7.3. L'instruction while

Le while permet de réaliser le même traitement en boucle tant qu'une condition est vraie :

  1. i = 1
  2. do_not_exit = True
  3.  
  4. while i < 10 and do_not_exit:
  5.     print(i)
  6.     i = i + 3
  7.     if i * i > 20:
  8.         do_not_exit = False
  9.    
1
4	

1.7.4. L'instruction for

Le for est utilisé pour parcourir containers (list, dict) ou des itérateurs (range) :

  1. # i = 0, .., 9
  2. for i in range(10):
  3.     print(i)
  4.  
  5. # i = 1, .., 9
  6. for i in range(1,10):
  7.     print(i)
  8.  
  9. l = ['renault','peugeot','fiat','volkswagen','audi']
  10.  
  11. # 'renault','peugeot',.., audi
  12. for marque in l:
  13.     print(marque)
  14.  
  15. # 0 renault, 1 peugeot, .., 4 audi
  16. for cle, marque in enumerate(l):
  17.     print(cle, marque)
  18.  
  19.  
  20. dico = { 0: 'noir', 1:'bleu', 2:'rouge', 3:'vert'}
  21.  
  22. # 0 noir, 1 bleu, .., 3 vert
  23. for cle in dico:
  24.     print(cle, dico[cle])
  25.  
  26. # 0 noir, 1 bleu, .., 3 vert
  27. for cle, valeur in dico.items():
  28.     print(cle, valeur)
  29.  
0
1
2
3
4
5
6
7
8
9

1
2
3
4
5
6
7
8
9

renault
peugeot
fiat
volkswagen
audi

0 renault
1 peugeot
2 fiat
3 volkswagen
4 audi
0 noir
1 bleu
2 rouge
3 vert
0 noir
1 bleu
2 rouge
3 vert

1.7.5. L'instruction print

Elle permet l'affichage d'information à l'écran ou l'enregistrement d'information dans un fichier au format texte. Son prototype est le suivant :

print(*args, sep=' ', end='\n', file=None, flush=False)

On affiche un ensemble d'arguments (args) qui sont des entiers, des flottant, decs chaînes, etc.

  1. print("Bonjour", "le", sep='***', end='')
  2. print("--monde--", end='!\n')
Bonjour***le--monde--!

1.7.6. L'instruction input

Elle permet de récupérer une chaîne saisie au clavier et terminée par un retour chariot qui ne sera pas pris en compte. Son format est le suivant :

input(prompt='')

saisir un entier:70
<class 'str'> <class 'int'>
70
saisir 3 prénoms: jean-michel karla sarah
jean-michel karla sarah

1.8. Les sous-programmes (procédures et fonctions)

Un sous-programme est introduit par le mot clé def suivi du nom du sous-programme et de ses paramètres :

  1. def ma_fonction(x, y):
  2.     return x + y
  3.  
  4. print(ma_fonction(1,2))
  5.  

Il est nécessaire d'ajouter des commentaires aux sous-programmes afin d'indiquer à d'autres personnes qui voudraient les utiliser, comment ils fonctionnent :

  1. def addition( x, y : int) -> int:
  2.     """
  3.        Calcule la somme de x et y
  4.    """
  5.  
  6.     return x + y
  7.  
  8. def car(l : list[int]) -> int:
  9.     """
  10.        Implantation de la fonction Lisp car (Contents of
  11.        the Address part of the Register) qui retourne
  12.        la liste passée en paramètre sans son premier
  13.        élément
  14.  
  15.        Si la liste est vide une exception est levée.
  16.    """
  17.     if len(l) == 0:
  18.         raise ValueError("liste vide")
  19.    
  20.     return l[0]
  21.    
  22. def cdr(l : list[int]) -> int:
  23.     """
  24.        Implantation de la fonction Lisp cdr (Contents of
  25.        the Decrement part of the Register) qui retourne
  26.        la liste passée en paramètre sans son premier
  27.        élément
  28.  
  29.        Si la liste est vide une exception est levée.
  30.    """
  31.     if len(l) == 0:
  32.         raise ValueError("liste vide")
  33.    
  34.     x = l.pop(0)
  35.     return l
  36.  
  37. x = 1
  38. y = 2
  39. print(f"{x}+{y} = ", addition(x,y))
  40.  
  41. ma_liste=[1,2,3]
  42. premier = car(ma_liste)
  43. ma_liste = cdr(ma_liste)
  44. print(f"premier élément de la liste = {premier}")
  45. print(f"liste sans son premier élément = {ma_liste}")
  46.  

1.8.1. Valeur de retour

Une fonction peut retourner plusieurs valeurs :

  1. def quotient_reste(x, y):
  2.     return x // y, x % y
  3.  
  4. # a=quotient, b=reste
  5. a, b = quotient_reste(77,8)
  6. print(a,b)
  7.  
  8. # (quotient,reste) tuple
  9. t = quotient_reste(59,8)
  10. print(t)
  11.  
  12. # ignore la première valeur de retour
  13. _, b = quotient_reste(59,8)
  14. print(b)
  15.  
9 5
(7, 3)
3

1.8.2. Passage par valeur et par adresse

En Python, il n'existe pas de passage de paramètres par adresse à proprement parler, comme dans des langages comme C ou C++. Cependant, Python utilise un mécanisme appelé passage par objet-référence qui implique que :

  • les objets mutables (comme les listes, dictionnaires, ou objets personnalisés) peuvent être modifiés directement dans une fonction, ce qui simule un comportement proche du passage par adresse
  • les objets immuables (comme les entiers, chaînes de caractères, ou tuples) ne peuvent pas être modifiés directement dans une fonction. Si vous modifiez leur valeur, cela crée un nouvel objet, et la modification ne s'applique pas à la variable d'origine

Pour simuler un passage par adresse avec des objets immuables, on peut encapsuler la valeur dans un objet mutable comme une liste ou un dictionnaire :

  1. def ma_fonction(param):
  2.     """
  3.      Simulation d'un passage par adresse
  4.    """
  5.     param[0] += 1  
  6.  
  7. valeur = 10
  8. parametre = [valeur]
  9. ma_fonction(parametre)
  10. valeur = parametre[0]
  11. # Affiche 11
  12. print(valeur)  

1.8.3. Typage des paramètres et du type de retour

Le typage des paramètres et des valeurs de retour des fonctions a pour objectif :

Cependant, le typage en Python :

Par exemple dans le code suivant, bien qu'on ait spécifié que la fonction addition prenait des paramètres entiers, il est toujours possible de passer des chaînes :

  1. def addition(a: int, b: int) -> int:
  2.     return a + b
  3.  
  4. # Appel avec des types inattendus
  5. print(addition("1", "2"))  # Résultat : "12" (concaténation de chaînes)
  6.  

Voici quelques exemples de typage de base ainsi que l'utilisation du package typing :

  1. # -----------------------------------------------------
  2. # définition du type des paramètres
  3. def enregistrer_utilisateur(nom: str, age: int, actif: bool) -> None:
  4.     pass
  5.  
  6. # -----------------------------------------------------
  7. # cas d'un dictionnaire
  8. def transformer_dict(data: dict[str, int]) -> dict[int, str]:
  9.     return {value: key for key, value in data.items()}
  10.  
  11. # -----------------------------------------------------
  12. # utilisation de tuple
  13. def extraire_coordonnees(coordonnees: tuple[float, float]) -> float:
  14.     x, y = coordonnees
  15.     return (x**2 + y**2)**0.5
  16.  
  17. # -----------------------------------------------------
  18. # typage avec liste d'entiers
  19. def afficher_nombres(nombres: list[int] | None) -> None:
  20.     if nombres is None:
  21.         print("Pas de nombres à afficher.")
  22.     else:
  23.         print(nombres)
  24.  
  25. # -----------------------------------------------------
  26. # union de valeurs
  27. def calculer(valeur: int | float) -> float:
  28.     return valeur ** 2
  29.  
  30. # -----------------------------------------------------
  31. # Any pour les listes génériques
  32. from typing import Any, List
  33.  
  34. def afficher_elements(elements: List[Any]) -> None:
  35.     for element in elements:
  36.         print(element)
  37.  
  38. # -----------------------------------------------------
  39. # combiner List et Union
  40. from typing import Union, List
  41.  
  42. def traiter_donnees(donnees: List[Union[int, str]]) -> None:
  43.     for donnee in donnees:
  44.         if isinstance(donnee, int):
  45.             print(f"Nombre : {donnee}")
  46.         else:
  47.             print(f"Texte : {donnee}")
  48.  
  49. # -----------------------------------------------------
  50. # utilisation de Dict
  51. from typing import Dict
  52.  
  53. def process_users(users: Dict[str, int]) -> None:
  54.     for name, age in users.items():
  55.         print(f"{name} a {age} ans")
  56.  
  57. # -----------------------------------------------------
  58. # utilisation de Literal
  59. from typing import Literal
  60.  
  61. def choisir_action(action: Literal["start", "stop", "pause"]) -> None:
  62.     print(f"Action choisie : {action}")
  63.  
  64. # -----------------------------------------------------
  65. # utilisation de TypeVar pour liste générique
  66. from typing import TypeVar
  67.  
  68. T = TypeVar("T")
  69.  
  70. def inverser_liste(elements: list[T]) -> list[T]:
  71.     return elements[::-1]
  72.  
  73.  

1.8.4. Les fonctions intégrées

Une fonction intégrée est une fonction propre au langage et peut parfois être appliquée à différents types :

1.8.4.a  Les fonctions de base

1.8.4.b  Les fonctions numériques

1.8.4.c  Les fonctions liées aux chaînes et objets itérables

1.8.4.d  Les fonctions liées aux types

1.8.4.e  Les fonctions liées aux entrées / sorties

1.9. Packages et import

Lorsque l'on crée un fichier Python, l'ensemble des variables globales et sous-programmes qu'il définit peuvent être réutilisés dans un autre fichier.

Imaginons que vous avons le fichier suivant :

  1. """
  2.   Package de calcul : définition d'une fonction f(x) et
  3.   d'une fonction f_prime qui calcule la dérivée pour
  4.   une fonction f(x) existante
  5. """
  6.  
  7. # définition d'une constante
  8. EPSILON = 1e-8
  9.  
  10.  
  11. def f(x : float) -> float:
  12.     """
  13.        Calcule f(x) = x**2 pour tout x flottant
  14.    """
  15.  
  16.     return x**2
  17.  
  18. def f_prime(x : float) -> float:
  19.     """
  20.        Calcule la dérivée de f(x) grâce à la formule
  21.  
  22.        f'(x) = (f(x+epsilon) - f(x)) / epsilon
  23.    """
  24.  
  25.     return (f(x+EPSILON) - f(x)) / EPSILON
  26.  
  27.  

Afin d'utiliser dans un autre fichier Python, les sous-programmes qu'il définit, trois possibilités s'offrent à nous :

1.9.1. import du fichier

  1. # import du fichier mais utilisation du nom du package
  2. # en tant que préfixe pour accèder aux sous-programmes
  3. # du fichier fonction.py
  4. import fonction
  5.  
  6. import numpy as np
  7.  
  8. x = np.arange(-2.0 ,2.0, 0.5)
  9. print(x)
  10.  
  11. # on utilise le préfixe 'fonction' devant f(x) et f_prime(x)
  12. y = fonction.f(x)
  13. y_prime = fonction.f_prime(x)
  14.  
  15. for i in range(len(y)):
  16.     print(f"{x[i]:5.3f} {y[i]:5.2f} {y_prime[i]}")
  17.  
  18.  

1.9.2. import du fichier avec renommage

  1. # import du fichier et renommage du fichier en tant que 'fn'
  2. # utilisé en tant que préfixe pour accèder aux sous-programmes
  3. # du fichier fonction.py
  4. import fonction as fn
  5.  
  6. import numpy as np
  7.  
  8. x = np.arange(-2.0 ,2.0, 0.5)
  9. print(x)
  10.  
  11. # on utilise le préfixe 'fn' devant f(x) et f_prime(x)
  12. y = fn.f(x)
  13. y_prime = fn.f_prime(x)
  14.  
  15. for i in range(len(y)):
  16.     print(f"{x[i]:5.3f} {y[i]:5.2f} {y_prime[i]}")

1.9.3. import de tous les éléments du fichier

  1. # import de tous les éléments (constantes et sous-programmes)
  2. # du fichier fonction.py de manière à ne pas utiliser de préfixe
  3. from fonction import *
  4.  
  5. import numpy as np
  6.  
  7. x = np.arange(-2.0 ,2.0, 0.5)
  8. print(x)
  9.  
  10. # on utilise directement f(x) et f_prime(x)
  11. y = f(x)
  12. y_prime = f_prime(x)
  13.  
  14. for i in range(len(y)):
  15.     print(f"{x[i]:5.3f} {y[i]:5.2f} {y_prime[i]}")

1.9.4. version d'un package

Certains packages nécessitent une version particulière d'un autre package pour pouvoir fonctionner correctement.

On peut connaître la version d'un package :

Soit en ligne de commande :

\$ pip list | grep numpy
numpy                       1.26.4

Soit dans l'interpréteur Python ou n exécutant un programme :

\$ python3
Python 3.12.3 (main, Nov  6 2024, 18:32:19) [GCC 13.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import numpy
>>> print(numpy.__version__)
1.26.4

1.10. Amélioration de l'efficacité du code

Voici un petit exemple qui montre comment utiliser pythran afin d'améliorer le temps d'exécution d'un programme Python.

Pythran est un compilateur Python vers C++ conçu pour accélérer les calculs numériques et optimiser les performances des programmes Python, en particulier ceux utilisant des tableaux NumPy. Il transforme des fonctions Python annotées en extensions Python compilées en C++, ce qui réduit considérablement le temps d'exécution pour des tâches computationnelles lourdes.

1.10.1. Sans pythran

On considère le module suivant qui fournit une fonction somme_sin_cos :

  1. import math
  2.  
  3. # pythran export somme_sin_cos(float64[])
  4. def somme_sin_cos(t):
  5.     """
  6.        Calcul de la somme des sin(x)*cos(x)
  7.        pour tout x de la liste t
  8.    """
  9.     total = 0.0
  10.     for x in t:
  11.         total += math.sin(x) * math.cos(x)
  12.     return total
  13.  
  14.  
  15.  

Ainsi que le programme qui utilise cette fonction :

  1. import numpy as np
  2. import calcul  # The compiled module
  3. import time
  4.  
  5. # tableau de 100 millions de valeurs aléatoires
  6. tableau = np.random.rand(100000000)
  7.  
  8. debut = time.time()
  9. resultat = calcul.somme_sin_cos(tableau)
  10. fin = time.time()
  11.  
  12. print("temps d'exécution=", (fin-debut))
  13. print(f"résultat={resultat}")
  14.  
  15.  

Si on exécute simplement ce programme en Python sur un AMD Ryzen 5 5600G, on obtient :

\$ time python3 utilisation_calcul.py
temps d'exécution= 7.702448606491089
résultat=35401505.8430852

real	0m8,248s
user	0m8,253s
sys	0m0,753s

Soit un temps d'exécution total de $8,253$ secondes dont $7,702$ secondes pour exécuter le code de la fonction somme_sin_cos.

1.10.2. Avec pythran

Il suffit de précompiler le code grâce à la commande qui suit, ce qui crée le fichier calcul.cpython-312-x86_64-linux-gnu.so :

\$ pythran calcul.py
\$ ls *.so
calcul.cpython-312-x86_64-linux-gnu.so

Un fichier avec l’extension .so sous Linux est une bibliothèque partagée (Shared Object). Il s'agit d’un fichier contenant du code compilé et des données qui peuvent être utilisés par plusieurs programmes en même temps. Ces fichiers sont comparables aux fichiers .dll sur Windows.

Ici cela ne fonctionne uniquement car on a ajouté une directive :

pythran export somme_sin_cos(float64[])

devant la fonction à optimiser qui indique quel type de donnée est fourni en entrée à la fonction. Pythran génère alors du code assembleur tout comme le ferait un compilateur C/C++.

Si maintenant on exécute le code comme précédemment, Python va utiliser le fichier objet partagé, mais cela est transparent pour l'utilisateur :

\$ time python3 utilisation_calcul.py
temps d'exécution= 0.9642660617828369
résultat=35404787.98554713

real	0m1,505s
user	0m1,502s
sys	0m0,761s

On met alors moins d'une seconde pour exécuter la fonction compilée.

Dans le fichier .so, on retrouve le code assembleur qui correspond au calcul :

3d80:       movsd  xmm0,QWORD PTR [r15]                  ; charger x ou t[i]
3d85:       mov    rsi,rbp 
3d88:       mov    rdi,r12
3d8b:       add    r15,0x8
3d8f:       call   2420 < sincos@plt>                    ; calculer sin(x) et cos(x)
3d94:       movsd  xmm0,QWORD PTR [rsp+0x18]               
3d9a:       mulsd  xmm0,QWORD PTR [rsp+0x10]             ; multiplier sin(x) par cos(x)
3da0:       addsd  xmm0,QWORD PTR [rsp]                  ; ajouter au résultat
3da5:       movsd  QWORD PTR [rsp],xmm0
3daa:       cmp    rbx,r15                               ; continuer tant qu'on est pas
3dad:       jne    3d80 <PyInit_calcul@@Base+0xa10>      ; à la fin du tableau
3daf:       lea    rdi,[rsp+0x40]

La fonction sincos appartient normalement à la librairie mathématique et son protoype est __sincos (double x, double *sinx, double *cosx).

Pour résumer :

 Sans Pythran   Avec Pythran 
 7,70s   0,96s 
Optimisation Pythran sur exemple avec AMD Ryzen 5 5600G

On notera cependant qu'on obtient pas le même résultat car les valeurs ne sont pas les mêmes dans les deux cas.