Polycopié pour le cours de Python

Cours dispensé à l’université de Rennes 2

Romain Tavenard

1 Introduction

Ce document est une tentative de polycopié associé au module de Python pour la deuxième année de licence MIASHS de l’Université de Rennes 2. Il est distribué librement (sous licence CC BY-NC-SA plus précisément) et se veut évolutif, n’hésitez donc pas à faire vos remarques à son auteur dont vous trouverez le contact sur sa page web. Ce polycopié a notamment bénéficié des apports d’Aurélie Lemaitre et d’Agnès Maunoury.

Durant la lecture de ce polycopié, vous trouverez des blocs de code tels que celui-ci :

def f(v):
    return v ** 2

x = 5
y = f(3 * x + 2)
print(y)
# [Sortie] 289

Nous prendrons notamment l’habitude de reporter les valeurs affichées par l’exécution du programme considéré dans un terminal avec la syntaxe utilisée à la dernière ligne du code ci-dessus.

Dans ce document, nous allons donc nous intéresser au langage Python. Pour tester les exemples présentés au fil de ce document ou réaliser les exercices proposés, vous aurez deux possibilités. La première consiste à ouvrir une console Python, à l’aide de la commande suivante (si vous êtes sous Unix, en supposant que le symbole $ corresponde au prompt de votre shell) :

$ python
Python 3.5.1 (default, Dec  9 2015, 11:28:16)
[GCC 4.2.1 Compatible Apple LLVM 7.0.0 (clang-700.1.76)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>

Lors de l’exécution de cette commande, on peut remarquer plusieurs choses. Tout d’abord, au démarrage, la console Python nous indique la version de Python qui est exécutée. Cela est important, car il existe notamment une importante différence entre les versions 2 (2.x.y) et 3 (3.x.y) de Python. Dans ce document, nous supposons l’utilisation de Python dans sa version 3, comme dans la console affichée plus haut. Enfin, une fois la console démarrée, on voit apparaître un prompt Python (>>>) qui indique que vous pouvez, à partir de ce point, entrer du code Python et en demander l’exécution en appuyant sur la touche retour chariot (ou “Entrée”) de votre clavier.

L’autre façon de programmer en Python, plus adaptée dès lors que l’on souhaite conserver une trace de ce qu’on a écrit, consiste à enregistrer vos commandes dans un fichier texte (en respectant la convention qui consiste à utiliser l’extension .py pour le nom de fichier) puis à faire exécuter votre programme par l’interpréteur Python :

$ python nom_de_mon_fichier.py
[...]

2 Structures de données et structures de contrôle

Dans ce chapitre, on s’intéresse aux éléments de base de la syntaxe Python : les structures de données d’une part et les structures de contrôle d’autre part. Les structures de données vont permettre de stocker dans la mémoire de l’ordinateur (dans le but de les traiter ensuite) des données tandis que les structures de contrôle vont servir à définir nos interactions avec ces données.

2.1 Variables

En Python, les données sont stockées dans des variables. On ne peut pas, comme c’est le cas dans d’autres langages, définir de constante (qui sont, dans ces langages, des moyens de stocker des valeurs n’ayant pas vocation à être modifiées au cours de l’exécution du programme). Une variable est une association entre un symbole (le nom de la variable) et une valeur, cette dernière pouvant varier au cours de l’exécution du programme.

2.1.1 Types des variables Python

Les types de base existant en Python sont les suivants :

De plus, il existe un type spécial (NoneType) ne permettant qu’une seule valeur : la valeur None qui signifie “pas de valeur” ou “valeur manquante”.

Le choix du type utilisé pour une variable impliquera :

Les variables Python sont typées dynamiquement, ce qui signifie qu’une variable, à un moment donné de l’exécution d’un programme, a un type précis qui lui est attribué, mais que celui-ci peut évoluer au cours de l’exécution du programme. En Python, le type d’une variable n’est pas déclaré par l’utilisateur : il est défini par l’usage (la valeur effective que l’on décide de stocker dans la variable en question).

Par exemple, l’instruction suivante (dite opération d’affectation) en Python attribue la valeur 12 à la variable v, qui devient donc automatiquement de type entier :

v = 12

Ainsi, les instructions suivantes ont toutes une incidence sur le type des variables considérées :

v = 12     # v est alors de type entier
c = "abc"  # c est de type chaîne de caractères
d = 'abc'  # d est également de type chaîne de caractères
           # les contenus de c et d sont identiques
v = 12.    # v change de type et est désormais de type nombre à virgule

Pour vérifier le type d’une variable, il suffit d’utiliser la fonction type de la librairie standard :

print(type(v))  # la fonction print(.) permet d'afficher
                # une information dans le terminal
# [Sortie] <class 'float'>

2.1.2 Opération d’affectation

Comme le montrent les exemples précédents, pour pouvoir utiliser des variables, on doit leur donner un nom (placé à gauche du signe égal dans l’opération d’affectation). Ces noms de variables doivent respecter certaines contraintes :

and del for is raise assert elif from lambda return break else global
not try nonlocal True False class except if or while continue import
pass yield None def finally in as with

Les noms de variable en Python sont sensibles à la casse, ainsi les variables maVariable et mavariable ne pointent pas sur les mêmes données en mémoire. Pour s’en convaincre, on peut exécuter le code suivant :

mavariable = 12
maVariable = 15
print(mavariable)
# [Sortie] 12
print(maVariable)
# [Sortie] 15

Comme on l’a vu plus haut, on utilise en Python l’opérateur = pour affecter une valeur à une variable. La sémantique de cet opérateur est la suivante : “affecter la valeur contenue dans le membre de droite à la variable du membre de gauche”. Ainsi, il est tout à fait valide d’écrire, en Python :

x = 3.9 * x * (1 - x)

Pour exécuter cette instruction, l’interpréteur Python commencera par évaluer le membre de droite en utilisant la valeur courante de la variable x, puis affectera la valeur correspondant au résultat de l’opération 3.9 * x * (1 - x) dans la variable x.

Ainsi, voici le résultat de l’exécution suivante :

x = 2
print(x)
# [Sortie] 2
x = 3.9 * x * (1 - x)
print(x)
# [Sortie] -7.8

Si l’on souhaite obtenir un affichage plus riche, on pourra utiliser la méthode format comme suit :

x = 2
x = 3.9 * x * (1 - x)
print("La valeur courante de x est {}".format(x))
# [Sortie] La valeur courante de x est -7.8

2.1.3 Opérateurs et priorité

On le voit dans l’exemple précédent, pour manipuler des variables, on utilisera des opérateurs (dont les plus connus sont les opérateurs arithmétiques). Le tableau suivant dresse une liste des opérateurs définis pour les variables dont le type est l’un des types numériques (entier, nombre à virgule, nombre complexe) :

Opérateur Opération
+ Addition
- Soustraction
* Multiplication
/ Division
** Élévation à la puissance
% Modulo (non défini pour les nombres complexes)
// Division

De plus, pour chacun de ces opérateurs, il existe un opérateur associé qui réalise successivement l’opération demandée puis l’affectation de la nouvelle valeur à la variable en question. Ainsi, l’instruction suivante :

x = x + 2

qui ajoute 2 à la valeur courante de x puis stocke le résultat du calcul dans x peut se réécrire :

x += 2

Ceci est purement un raccourci de notation, s’il ne vous semble pas évident à maîtriser au premier abord, vous pouvez vous en passer et toujours utiliser la notation x = x + 2.

Enfin, lorsque l’évaluation d’une expression implique plusieurs opérateurs, les règles de priorité sont les suivantes (de la priorité maximale à la priorité minimale) :

  1. parenthèses ;
  2. élévation à la puissance ;
  3. multiplication / division ;
  4. addition / soustraction ;
  5. de gauche à droite.

Pour prendre un exemple concret, pour évaluer l’expression :

3.9 * x * (1 - x)

l’interpréteur Python commencera par évaluer le contenu de la parenthèse puis, les 2 opérations restantes étant toutes des multiplications, il les effectuera de gauche à droite.

De plus, lorsqu’une opération est effectuée entre deux variables de types différents, le type le plus générique est retenu. Par exemple, si l’on multiplie un entier par un nombre à virgule, le résultat sera de type float. De même, le résultat de l’addition entre un nombre complexe et un nombre à virgule est un complexe.

Attention. Comme indiqué en introduction, ce polycopié suppose que vous utilisez Python dans sa version 3. Il est à noter qu’il existe une différence importante entre Python 2 et Python 3 dans la façon d’effectuer des opérations mêlant nombres entiers et flottants. Par exemple, l’opération suivante :

x = 2 / 3

stockera, en Python 2, la valeur 0 (résultat de la division entière de 2 par 3) dans la variable x alors qu’en Python 3, la division flottante sera effectuée et ainsi x contiendra 0.666666... En Python 3, si l’on souhaite effectuer une division entière, on pourra utiliser l’opérateur // :

print(2 // 3)
# [Sortie] 0

2.2 Structures de contrôle

Un programme est une séquence d’instructions dont l’ordre doit être respecté. Au-delà de cet aspect séquentiel, on peut souhaiter :

Les structures de contrôle associées à ces différents comportements sont décrits dans la suite de cette section.

2.2.1 Structures conditionnelles

On peut indiquer à un programme de n’exécuter une instruction (ou une séquence d’instructions) que si une certaine condition est remplie, à l’aide du mot-clé if :

x = 12
if x > 0:
    print("X est positif")
    print("X n'est pas négatif")
# [Sortie] X est positif
# [Sortie] X n'est pas négatif

On remarque ici que la condition est terminée par le symbole :, de plus, la séquence d’instructions à exécuter si la condition est remplie est indentée, cela signifie qu’elle est décalée d’un “cran” (généralement une tabulation ou 4 espaces) vers la droite. Cette indentation est une bonne pratique recommandée quel que soit le langage que vous utilisez, mais en Python, c’est même une obligation (sinon, l’interpréteur Python ne saura pas où commence et où se termine la séquence à exécuter sous condition).

Dans certains cas, on souhaite exécuter une série d’instructions si la condition est vérifiée et une autre série d’instructions si elle ne l’est pas. Pour cela, on utilise le mot-clé else comme suit :

x = -1
if x > 0:
    print("X est positif")
    print("X n'est pas négatif")
else:
    print("X est négatif")
# [Sortie] X est négatif

Là encore, on remarque que l’indentation est de rigueur pour chacun des deux blocs d’instructions. On note également que le mot-clé else se trouve au même niveau que le if auquel il se réfère.

Enfin, de manière plus générale, il est possible de définir plusieurs comportements en fonction de plusieurs tests successifs, à l’aide du mot-clé elif. elif est une contraction de else if, qui signifie sinon si.

x = -1
if x > 0:
    print("X est positif")
    x = 4
elif x > -2:
    print("X est compris entre -2 et 0")
elif x > -4:
    print("X est compris entre -4 et -2")
else:
    print("X est inférieur à -4")
# [Sortie] X est compris entre -2 et 0

Pour utiliser ces structures conditionnelles, il est important de maîtriser les différents opérateurs de comparaison à votre disposition en Python, dont voici une liste non exhaustive :

Opérateur Comparaison effectuée Exemple
< Plus petit que x < 0
> Plus grand que x > 0
<= Plus petit ou égal à x <= 0
>= Plus grand ou égal à x >= 0
== Égal à x == 0
!= Différent de x != 0
is Test d’égalité pour le cas de la valeur None x is None
is not Test d’inégalité pour le cas de la valeur None x is not None
in Test de présence d’une valeur dans une liste x in [1, 5, 7]

Il est notamment important de remarquer que, lorsque l’on souhaite tester l’égalité entre deux valeurs, l’opérateur à utiliser est == et non = (qui sert à affecter une valeur à une variable).

2.2.2 Boucles

Il existe, en Python comme dans une grande majorité des langages de programmation, deux types de boucles :

2.2.2.1 Boucles while

Les premières ont une syntaxe très similaire à celle des structures conditionnelles simples :

x = 0
while x <= 10:
    print(x)
    x = 2 * x + 2
# [Sortie] 0
# [Sortie] 2
# [Sortie] 6

On voit bien ici, en analysant la sortie produite par ces quelques lignes, que le contenu de la boucle est répété plusieurs fois. En pratique, il est répété jusqu’à ce que la variable x prenne une valeur supérieure à 10 (14 dans notre cas). Il faut être très prudent avec ces boucles while car il est tout à fait possible de créer une boucle dont le programme ne sortira jamais, comme dans l’exemple suivant :

x = 2
y = 0
while x > 0:
    y = y - 1
    print(y)
print("Si on arrive ici, on a fini")

En effet, on a ici une boucle qui s’exécutera tant que x est positif, or la valeur de cette variable est initialisée à 2 et n’est pas modifiée au sein de la boucle, la condition sera donc toujours vérifiée et le programme ne sortira jamais de la boucle. Pour information, si vous vous retrouvez dans un tel cas, vous pourrez interrompre l’exécution du programme à l’aide de la combinaison de touches Ctrl + C.

2.2.2.2 Boucles for

Le second type de boucle repose en Python sur l’utilisation de listes (ou, plus généralement, d’itérables) dont nous reparlerons plus en détail dans la suite de cet ouvrage. Sachez pour le moment qu’une liste est un ensemble ordonné d’éléments. On peut alors exécuter une série d’instructions pour toutes les valeurs d’une liste :

for x in [1, 5, 7]:
    print(x)
print("Fin de la boucle")
# [Sortie] 1
# [Sortie] 5
# [Sortie] 7
# [Sortie] Fin de la boucle

Cette syntaxe revient à définir une variable x qui prendra successivement pour valeur chacune des valeurs de la liste [1, 5, 7] dans l’ordre et à exécuter le code de la boucle (ici, un appel à la fonction print) pour cette valeur de la variable x.

2.2.3 Fonctions

Nous avons déjà vu dans ce qui précède, sans le dire, des fonctions. Par exemple, lorsque l’on écrit :

print(x)

on demande l’appel à une fonction, nommée print et prenant un argument (ici, la variable x). La fonction print ne retourne pas de valeur, elle ne fait qu’afficher la valeur contenue dans x sur le terminal. D’autres fonctions, comme type dont nous avons parlé plus haut, retournent une valeur et cette valeur peut être utilisée dans la suite du programme, comme dans l’exemple suivant :

x = type(1)  # On stocke dans x la valeur retournée par type
y = type(2.)
if x == y:
    print("types identiques")
else:
    print("types différents")

2.2.3.1 Définition d’une fonction

Lorsqu’un ensemble d’instructions est susceptible d’être utilisé à plusieurs occasions dans un ou plusieurs programmes, il est recommandé de l’isoler au sein d’une fonction. Cela présentera les avantages suivants :

Pour définir une fonction en Python, on utilise le mot-clé def :

def f(x):
    y = 5 * x + 2
    z = x + y
    return z // 2

On a ici défini une fonction

Il est possible, en Python, d’écrire des fonctions retournant plusieurs valeurs. Pour ce faire, ces valeurs seront séparées par des virgules dans l’instruction return :

def f(x):
    y = 5 * x + 2
    z = x + y
    return z // 2, y

Enfin, en l’absence d’instruction return, une fonction retournera la valeur None.

Il est également possible d’utiliser le nom des arguments de la fonction lors de l’appel, pour ne pas risquer de se tromper dans l’ordre des arguments. Par exemple, si l’on a la fonction suivante :

def affiche_infos_personne(poids, taille):
    print("Poids: ", poids)
    print("Taille: ", taille)

Les trois appels suivants sont équivalents :

affiche_infos_personne(80, 180)
# [Sortie] Poids: 80
# [Sortie] Taille: 180
affiche_infos_personne(taille=180, poids=80)
# [Sortie] Poids: 80
# [Sortie] Taille: 180
affiche_infos_personne(poids=80, taille=180)
# [Sortie] Poids: 80
# [Sortie] Taille: 180

Notons qu’il est alors possible d’interchanger l’ordre des arguments lors de l’appel d’une fonction si on précise leur nom. Évidemment, pour que cela soit vraiment utile, il est hautement recommandé d’utiliser des noms d’arguments explicites lors de la définition de vos fonctions.

2.2.3.2 Argument(s) optionnel(s) d’une fonction

Certains arguments d’une fonction peuvent avoir une valeur par défaut, décidée par la personne qui a écrit la fonction. Dans ce cas, si l’utilisateur ne spécifie pas explicitement de valeur pour ces arguments lors de l’appel à la fonction, c’est la valeur par défaut qui sera utilisée dans la fonction, dans le cas contraire, la valeur spécifiée sera utilisée.

Par exemple, la fonction print dispose de plusieurs arguments facultatifs, comme le caractère par lequel terminer l’affichage (par défaut, un retour à la ligne, "\n") :

print("La vie est belle")
# [Sortie] La vie est belle
print("Life is beautiful")
# [Sortie] Life is beautiful
print("La vie est belle", end="--")
print("Life is beautiful", end="--")
# [Sortie] La vie est belle--Life is beautiful--

Lorsque vous définissez une fonction, la syntaxe à utiliser pour donner une valeur par défaut à un argument est la suivante :

def f(x, y=0):  # La valeur par défaut pour y est 0
    return x + 5 * y

Attention toutefois, les arguments facultatifs (ie. qui disposent d’une valeur par défaut) doivent impérativement se trouver, dans la liste des arguments, après le dernier argument obligatoire. Ainsi, la définition de fonction suivante n’est pas correcte :

def f(x, y=0, z):
    return x - 2 * y + z

2.3 Les modules en Python

Jusqu’à présent, nous avons utilisé des fonctions (comme print) issues de la librairie standard de Python. Celles-ci sont donc chargées par défaut lorsque l’on exécute un script Python. Toutefois, il peut être nécessaire d’avoir accès à d’autres fonctions et/ou variables, définies dans d’autres librairies. Pour cela, il sera utile de charger le module correspondant.

Prenons l’exemple du module math qui propose un certain nombre de fonctions mathématiques usuelles (sin pour le calcul du sinus d’un angle, sqrt pour la racine carrée d’un nombre, etc.) ainsi que des constantes mathématiques très utiles comme pi. Le code suivant charge le module en mémoire puis fait appel à certaines de ses fonctions et/ou variables :

import math

print(math.sin(0))
# [Sortie] 0.0
print(math.pi)
# [Sortie] 3.141592653589793
print(math.cos(2 * math.pi))
# [Sortie] 1.0
print(math.sqrt(2))
# [Sortie] 1.4142135623730951

Vous remarquerez ici que l’instruction d’import du module se trouve nécessairement avant les instructions faisant référence aux fonctions et variables de ce module, faute de quoi ces dernières ne seraient pas définies. De manière générale, vous prendrez la bonne habitude d’écrire les instructions d’import en tout début de vos fichiers Python, pour éviter tout souci.

Exercice 2.1 Écrivez une expression conditionnelle, qui à partir d’une température d’eau stockée dans une variable t affiche dans le terminal si l’eau à cette température est à l’état liquide, solide ou gazeux.

Exercice 2.2 Écrivez une boucle permettant d’afficher tous les chiffres impairs inférieurs à une valeur n initialement fixée.

Exercice 2.3 Écrivez une fonction en Python qui prenne en argument une longueur l et retourne l’aire du triangle équilatéral de côté l.

Exercice 2.4 Écrivez une fonction en Python qui affiche tous les termes plus petits que 1000 de la suite (un)(u_n) définie comme : u0=2n1,un=un12\begin{array}{rcc}u_0 & = & 2 \\ \forall n \geq 1, \, u_n & = & u_{n-1}^2\end{array}

3 Les dates

Dans la partie précédente, nous avons présenté les types de base du langage Python et les opérateurs associés aux types numériques. Lorsque l’on souhaite manipuler des dates et des heures, on devra avoir recours à un autre type de données, défini dans le module datetime. Pour cela, il faudra commencer par charger ce module en ajoutant l’instruction :

import datetime

en en-tête de votre script Python.

Pour créer une nouvelle variable de ce type, on utilisera la syntaxe :

d = datetime.datetime(annee, mois, jour, heure, minutes)

La syntaxe datetime.datetime, qui peut vous sembler bizarre au premier coup d’oeil signifie à l’interpréteur Python qu’il doit chercher dans le module datetime une fonction dont le nom est datetime et l’appeler.

En fait, on pourrait rajouter lors de l’appel de datetime.datetime un argument pour spécifier les secondes, puis éventuellement un autre pour les microsecondes, si l’on avait besoin d’une heure plus précise. Si, au contraire, on ne spécifie pas l’heure lors de l’appel de la fonction, l’heure 00h00 sera choisie par défaut.

Par exemple :

import datetime  # Cette commande doit se trouver en début de fichier

# [...] (ici, du code concernant autre chose si besoin)

d = datetime.datetime(2019, 8, 27, 17, 23)
print(d)
# [Sortie] 2019-08-27 17:23:00

d = datetime.datetime(2019, 8, 27, 17, 23, 32)
print(d)
# [Sortie] 2019-08-27 17:23:32

d = datetime.datetime(2019, 8, 27)
print(d)
# [Sortie] 2019-08-27 00:00:00

Les opérateurs de comparaison vus au chapitre précédent (<, >, <=, >=, ==) fonctionnent de manière naturelle avec ce type de données :

d1 = datetime.datetime(2019, 8, 27, 17, 23)
d2 = datetime.datetime(2019, 8, 27, 17, 28)
d3 = datetime.datetime(2019, 8, 27, 17, 23)
print(d1 < d2)
# [Sortie] True

print(d1 == d3)
# [Sortie] True

print(d1 > d3)
# [Sortie] False

Il existe d’autres moyens de construire des variables de type date. On peut générer une date correspondant à l’heure actuelle avec la fonction datetime.now du module datetime :

date_actuelle = datetime.datetime.now()

3.1 Transformation d’une date en chaîne de caractères

Si l’on souhaite transformer une date en chaîne de caractères (par exemple pour l’afficher), on peut lui appliquer la fonction str :

print(str(datetime.datetime(2019, 8, 27)))

Dans ce cas, on ne peut pas gérer la façon dont se fait cette transformation. Pour contourner cette limitation, il convient alors d’utiliser strftime :

d1 = datetime.datetime(...)
s = d1.strftime(format)

L’attribut format que l’on passe à cette fonction va servir à définir comment on souhaite représenter la date en question. Il s’agit d’une chaîne de caractères qui pourra contenir les éléments suivants :

Code Signification
%Y Année
%m Mois
%d Jour
%H Heure
%M Minutes

Remarquez que la casse n’est pas neutre pour les codes à utiliser : %M et %m ont des significations tout à fait différentes. Notez également qu’il existe d’autres codes permettant de générer des chaînes de caractères plus variées encore. Une liste de ces codes est disponible sur la page d’aide du module datetime.

Vous pouvez vous référer aux exemples ci-dessous pour mieux comprendre le fonctionnement de la fonction strftime :

d = datetime.datetime(2019, 8, 27, 17, 23)

print(d.strftime("%d-%m-%Y, %H:%M"))
# [Sortie] 27-08-2019, 17:23

print(d.strftime("%d-%m-%Y"))
# [Sortie] 27-08-2019

print(d.strftime("%H:%M"))
# [Sortie] 17:23

print(d.strftime("%d/%m/%Y %Hh%M"))
# [Sortie] 27/08/2019 17h23

Il est également possible d’effectuer l’opération inverse (lire une date contenue dans une chaîne de caractères, étant donné un format connu). Cela se fait avec la fonction datetime.strptime (attention aux confusions possibles entre strftime et datetime.strptime) :

d1 = datetime.datetime.strptime(chaine_a_lire, format)

Voici deux exemples d’utilisation de cette fonction :

d1 = datetime.datetime.strptime("2019/8/27, 17:23", "%Y/%m/%d, %H:%M")
d2 = datetime.datetime.strptime("27-08-2019", "%d-%m-%Y")

3.2 Calcul de temps écoulé

On peut ensuite souhaiter calculer la différence entre deux dates. Le résultat de cette opération est une durée, représentée en Python par le type timedelta (lui aussi défini dans le module datetime).

d1 = datetime.datetime(2019, 8, 27, 17, 23)
d2 = datetime.datetime(2019, 8, 27, 17, 28)
intervalle_de_temps = d1 - d2
print(type(intervalle_de_temps))
# [Sortie] <class 'datetime.timedelta'>

Très souvent, il est utile pour manipuler une durée de la convertir en un nombre de secondes et de manipuler ce nombre ensuite. Cela se fait à l’aide de la commande :

d1 = datetime.datetime(2019, 8, 27, 17, 23)
d2 = datetime.datetime(2019, 8, 27, 17, 28)
intervalle_de_temps = d1 - d2
print(intervalle_de_temps.total_seconds())
# [Sortie] -300.0

On remarque ici que l’intervalle obtenu est négatif, ce qui était prévisible car il s’agit de l’intervalle d1 - d2 et on a d1 < d2.

Notez enfin que l’on peut tout à fait ajouter une durée à une date :

d1 = datetime.datetime(2019, 8, 27, 17, 23)
d2 = datetime.datetime(2019, 8, 27, 17, 28)
d3 = datetime.datetime(2019, 8, 27, 18, 00)
intervalle_de_temps = d2 - d1
print(d3 + intervalle de temps)
# [Sortie]  2019-08-27 18:05:00

Exercice 3.1 S’est-il écoulé plus de temps (i) entre le 2 Janvier 1920 à 7h32 et le 4 Mars 1920 à 5h53 ou bien (ii) entre le 30 Décembre 1999 à 17h12 et le 1er Mars 2000 à 15h53 ?

Exercice 3.2 À l’aide des fonctions du module datetime vues plus haut, affichez, pour chaque année civile comprise entre 2010 et 2030, si elle est bissextile ou non.

4 Les listes

En Python, les listes stockent des séquences d’éléments. Il n’est pas nécessaire que tous les éléments d’une liste soient du même type, même si dans les exemples que nous considérerons, ce sera souvent le cas.

On peut trouver des informations précieuses sur le sujet des listes dans l’aide en ligne de Python disponible à l’adresse : https://docs.python.org/3/tutorial/datastructures.html.

4.1 Avant-propos : listes et itérables

Dans la suite, nous parlerons de listes, qui est un type de données bien spécifique en Python. Toutefois, une grande partie de notre propos pourra se transposer à l’ensemble des itérables en Python (c’est-à-dire l’ensemble des objets Python dont on peut parcourir les éléments un à un).

Il existe toutefois une différence majeure entre listes et itérables : nous verrons dans la suite de ce chapitre que l’on peut accéder au ii-ème élément d’une liste simplement, alors que ce n’est généralement pas possible pour un itérable (pour ce dernier, il faudra parcourir l’ensemble de ses éléments et s’arrêter lorsque l’on est effectivement rendu au ii-ème).

Toutefois, si l’on a un itérable iterable, il est possible de le transformer en liste simplement à l’aide de la fonction list :

l = list(iterable)

4.2 Création de liste

Pour créer une liste contenant des éléments définis (par exemple la liste contenant les entiers 1, 5 et 7), il est possible d’utiliser la syntaxe suivante :

liste = [1, 5, 7]

De la même façon, on peut créer une liste vide (ne contenant aucun élément) :

liste = []
print(len(liste))
# [Sortie] 0

On voit ici la fonction len qui retourne la taille d’une liste passée en argument (ici 0 puisque la liste est vide).

Toutefois, lorsque l’on souhaite créer des listes longues (par exemple la liste des 1000 premiers entiers), cette méthode est peu pratique. Heureusement, il existe des fonctions qui permettent de créer de telles listes. Par exemple, la fonction range(a, b) retourne un itérable contenant les entiers de a (inclus) à b (exclu) :

l = range(1, 10)     # l = [1, 2, 3, ..., 9]
l = range(10)        # l = [0, 1, 2, ..., 9]
l = range(0, 10, 2)  # l = [0, 2, 4, ..., 8]

On remarque que, si l’on ne donne qu’un argument à la fonction range, l’itérable retourné débute à l’entier 0. Si, au contraire, on passe un troisième argument à la fonction range, cet argument correspond au pas utilisé entre deux éléments successifs.

4.3 Accès aux éléments d’une liste

Pour accéder au ii-ème élément d’une liste, on utilise la syntaxe :

l[i]

Attention, toutefois, le premier indice d’une liste est 0, on a donc :

l = [1, 5, 7]
print(l[1])
# [Sortie] 5
print(l[0])
# [Sortie] 1

On peut également accéder au dernier élément d’une liste en demandant l’élément d’indice -1 :

l = [1, 5, 7]
print(l[-1])
# [Sortie] 7
print(l[-2])
# [Sortie] 5
print(l[-3])
# [Sortie] 1

De la même façon, on peut accéder au deuxième élément en partant de la fin via l’indice -2, etc.

Ainsi, pour une liste de taille nn, les valeurs d’indice valides sont les entiers compris entre n-n et n1n - 1 (inclus).

Il est également à noter que l’accès aux éléments d’une liste peut se faire en lecture (lire l’élément stocké à l’indice i) comme en écriture (modifier l’élément stocké à l’indice i) :

l = [1, 5, 7]
print(l[1])
# [Sortie] 5
l[1] = 2
print(l)
# [Sortie] [1, 2, 7]

Enfin, on peut accéder à une sous-partie d’une liste à l’aide de la syntaxe l[d:f]d est l’indice de début et f est l’indice de fin (exclu). Ainsi, on a :

l = [1, 5, 7, 8, 0, 9, 8]
print(l[2:4])
# [Sortie] [7, 8]

Lorsque l’on utilise cette syntaxe, si l’on omet l’indice de début, la sélection commence au début de la liste et si l’on omet l’indice de fin, elle s’étend jusqu’à la fin de la liste :

l = [1, 5, 7, 8, 0, 9, 8]
print(l[:3])
# [Sortie] [1, 5, 7]
print(l[5:])
# [Sortie] [9, 8]

4.4 Parcours d’une liste

Lorsque l’on parcourt une liste, on peut vouloir accéder :

Ces trois cas de figure impliquent trois parcours de liste différents, décrits dans ce qui suit.

Attention. Quel que soit le parcours de liste utilisé, il est fortement déconseillé de supprimer ou d’insérer des éléments dans une liste pendant le parcours de celle-ci.

4.4.1 Parcours des éléments

Pour parcourir les éléments d’une liste, on utilise une boucle for :

l = [1, 5, 7]
for elem in l:
    print(elem)
# [Sortie] 1
# [Sortie] 5
# [Sortie] 7

Dans cet exemple, la variable elem va prendre successivement pour valeur chacun des éléments de la liste.

4.4.2 Parcours par indices

Pour avoir accès aux indices (positifs) de la liste, on devra utiliser un subterfuge. On sait que les indices d’une liste sont les entiers compris entre 0 (inclus) et la taille de la liste (exclu). On va donc utiliser la fonction range pour cela :

l = [1, 5, 7]
n = len(l)  # n = 3 ici
for i in range(n):
    print(i)
# [Sortie] 0
# [Sortie] 1
# [Sortie] 2

4.4.3 Parcours par éléments et indices

Dans certains cas, enfin, on a besoin de manipuler simultanément les indices d’une listes et les éléments associés. Cela se fait à l’aide de la fonction enumerate :

l = [1, 5, 7]
for i, elem in enumerate(l):
    print(i, elem)
# [Sortie] 0 1
# [Sortie] 1 5
# [Sortie] 2 7

On a donc ici une boucle for pour laquelle, à chaque itération, on met à jour les variables i (qui contient l’indice courant) et elem (qui contient l’élément se trouvant à l’indice i dans la liste l).

Pour tous ces parcours de listes, il est conseillé d’utiliser des noms de variables pertinents, afin de limiter les confusions dans la nature des éléments manipulés. Par exemple, on pourra utiliser i ou j pour noter des indices, mais on préfèrera elem ou val pour désigner les éléments de la liste.

Exercice 4.1 Écrivez une fonction en Python qui permette de calculer l’argmax d’une liste, c’est-à-dire l’indice auquel est stockée la valeur maximale de la liste.

4.5 Manipulations de listes

Nous présentons dans ce qui suit les opérations élémentaires de manipulation de listes.

4.5.1 Insertion d’élément

Pour insérer un nouvel élément dans une liste, on peut :

Comme vous pouvez le remarquer, il est ici question de méthodes et non plus de fonctions. Pour l’instant, sachez que les méthodes sont des fonctions spécifiques à certains objets, comme les listes par exemples. L’appel de ces méthodes est un peu particulier, comme vous pouvez le remarquer dans ce qui suit :

l = [1, 5, 7]
l.append(2)
print(l)
# [Sortie] [1, 5, 7, 2]
l.insert(2, 0)  # insère la valeur 0 à l'indice 2
print(l)
# [Sortie] [1, 5, 0, 7, 2]

4.5.2 Suppression d’élément

Si l’on souhaite, maintenant, supprimer un élément dans une liste, deux cas de figures peuvent se présenter. On peut souhaiter :

l = [1, 5, 7]
l.pop(1)  # l'élément d'indice 1 est le deuxième élément de la liste !
print(l)
# [Sortie] [1, 7]
l.pop()  # par défaut, supprime le dernier élément de la liste
print(l)
# [Sortie] [1]
l = [7, 5, 1]
l.remove(1) # supprime la première occurence de 1 dans la liste
print(l)
# [Sortie] [7, 5]

On peut noter que la méthode pop retourne la valeur supprimée, ce qui peut s’avérer utile :

l = [1, 5, 7]
v = l.pop(1)
print(v)
# [Sortie] 5
print(l)
# [Sortie] [1, 7]

4.5.3 Recherche d’élément

Pour trouver l’indice de la première occurrence d’une valeur dans une liste, on utilisera la méthode index :

l = [1, 5, 7]
print(l.index(7))
# [Sortie] 2

Si l’on ne cherche pas à connaître la position d’une valeur dans une liste mais simplement à savoir si une valeur est présente dans la liste, on peut utiliser le mot-clé in :

l = [1, 5, 7]
if 5 in l:
    print("5 est dans l")
# [Sortie] 5 est dans l

4.5.4 Création de listes composites

On peut également concaténer deux listes (c’est-à-dire mettre bout à bout leur contenu) à l’aide de l’opérateur + :

l1 = [1, 5, 7]
l2 = [3, 4]
l = l1 + l2
print(l)
# [Sortie] [1, 5, 7, 3, 4]

Dans le même esprit, l’opérateur * peut aussi être utilisé pour des listes :

l1 = [1, 5]
l2 = 3 * l1
print(l2)
# [Sortie] [1, 5, 1, 5, 1, 5]

Bien entendu, vu le sens de cet opérateur, on ne peut multiplier une liste que par un entier.

4.5.5 Tri de liste

Enfin, on peut trier les éléments contenus dans une liste à l’aide de la fonction sorted :

l = [4, 5, 2]
l2 = sorted(l)
print(l2)
# [Sortie] [2, 4, 5]

Exercice 4.2 Écrivez une fonction qui prenne deux listes en entrée et retourne l’intersection des deux listes (c’est-à-dire une liste contenant tous les éléments présents dans les deux listes).

Exercice 4.3 Écrivez une fonction qui prenne deux listes en entrée et retourne l’union des deux listes (c’est-à-dire une liste contenant tous les éléments présents dans au moins une des deux listes) sans doublon.

4.6 Copie de liste

Pour la plupart des variables, en Python, la copie ne pose pas de problème :

a = 12
b = a
a = 5
print(a, b)
# [Sortie] 5 12

Cela ne se passe pas de la même façon pour les listes. En effet, si l est une liste, lorsque l’on écrit :

l2 = l

on ne recopie pas le contenu de l dans l2, mais on crée une variable l2 qui va “pointer” vers la même position dans la mémoire de votre ordinateur que l. La différence peut sembler mince, mais cela signifie que si l’on modifie l même après l’instruction l2 = l, la modification sera répercutée sur l2 :

l = [1, 5, 7]
l2 = l
l[1] = 2
print(l, l2)
# [Sortie] [1, 2, 7] [1, 2, 7]

Lorsque l’on souhaite éviter ce comportement, il faut effectuer une copie explicite de liste, à l’aide par exemple de la fonction list :

l = [1, 5, 7]
l2 = list(l)
l[1] = 2
print(l, l2)
# [Sortie] [1, 2, 7] [1, 5, 7]

4.7 Bonus : listes en compréhension

Il est possible de créer des listes en filtrant et/ou modifiant certains éléments d’autres listes ou itérables. Supposons par exemple que l’on souhaite créer la liste des carrés des 10 premiers entiers naturels. Le code qui suit présente deux façons équivalentes de créer une telle liste :

# Façon "classique"
l = []
for i in range(10):
    l.append(i ** 2)
print(l)
# [Sortie] [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

# En utilisant les listes en compréhension
l = [i ** 2 for i in range(10)]
print(l)
# [Sortie] [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

On remarque que la syntaxe de liste en compréhension est plus compacte. On peut également appliquer un filtre sur les éléments de la liste de départ (ici range(10)) à considérer à l’aide du mot-clé if :

l = [i ** 2 for i in range(10) if i % 2 == 0]
print(l)
# [Sortie] [0, 4, 16, 36, 64]

Ici, on n’a considéré que les entiers pairs.

5 Les chaînes de caractères

Nous nous intéressons maintenant à un autre type de données particulier du langage Python : les chaînes de caractères (type str). Pour créer une chaîne de caractères, il suffit d’utiliser des guillemets, simples ou doubles (les deux sont équivalents) :

s1 = "abc"
s2 = 'bde'

Comme pour les listes (et peut-être même plus encore), il est fortement conseillé de se reporter à l’aide en ligne dédiée lorsque vous avez des doutes sur la manipulation de chaînes de caractères : https://docs.python.org/3/library/stdtypes.html#string-methods

5.1 Conversion d’une chaîne en nombre

Si une chaîne de caractères représente une valeur numérique (comme la chaîne "10.2" par exemple), on peut la transformer en un entier ou un nombre à virgule, afin de l’utiliser ensuite pour des opérations arithmétiques. On utilise pour cela les fonctions de conversion, respectivement int et float.

s = '10.2'
f = float(s)
print(f)
# [Sortie] 10.2
print(f == s)
# [Sortie] False
print(f + 2)
# [Sortie] 12.2
s = '10'
i = int(s)
print(i)
# [Sortie] 10
print(i == s)
# [Sortie] False
print(i - 1)
# [Sortie] 9

5.2 Analogie avec les listes

Les chaînes de caractères se manipulent en partie comme des listes. On peut ainsi obtenir la taille d’une chaîne de caractères à l’aide de la fonction len, ou accéder à la ii-ème lettre d’une chaîne de caractères avec la notation s[i]. Comme pour les listes, il est possible d’indicer une chaîne de caractères en partant de la fin, en utilisant des indices négatifs :

s = "abcdef"
print(len(s))
# [Sortie] 6
print(s[0])
# [Sortie] a
print(s[-1])
# [Sortie] f

De même, on peut sélectionner des sous-parties de chaînes de caractères à partir des indices de début et de fin de la sélection. Comme pour les listes, l’indice de fin correspond au premier élément exclu de la sélection :

s = "abcdef"
print(s[2:4])
# [Sortie] cd

Comme pour les listes, on peut concaténer deux chaînes de caractères à l’aide de l’opérateur + ou répéter une chaîne de caractères avec l’opérateur * :

s = "ab" + ('cde' * 3)
print(s)
# [Sortie] abcdecdecde

On peut également tester la présence d’une sous-chaîne de caractères dans une chaîne avec le mot-clé in :

s = "abcde"
print("a" in s)
# [Sortie] True
print("bcd" in s)
# [Sortie] True
print("bCd" in s)
# [Sortie] False

Attention. Toutefois, l’analogie entre listes et chaînes de caractères est loin d’être parfaite. Par exemple, on peut accéder au ii-ème élément d’une chaîne de caractères en lecture, mais pas en écriture. Si s est une chaîne de caractères, on ne peut pas exécuter s[2] = "c" par exemple.

5.3 Principales méthodes de la classe str

La liste de méthodes de la classe str qui suit n’est pas exhaustive, il est conseillé de consulter l’aide en ligne de Python pour plus d’informations.

Exercice 5.1 Écrivez une fonction qui prenne en argument deux chaînes de caractères s et prefix et retourne le nombre de mots de la chaîne s qui débutent par la chaîne prefix.

Exercice 5.2 Écrivez une fonction qui prenne en argument deux chaînes de caractères s et mot_cible et retourne le nombre d’occurrences du mot mot_cible dans la chaîne s en ne tenant pas compte de la casse.

6 Les dictionnaires

Comme une liste, un dictionnaire est une collection de données. Mais, à la différence des listes, les dictionnaires ne sont pas ordonnés (ou, tout du moins, ils le sont dans un ordre qui ne nous est pas naturel). Chaque entrée dans un dictionnaire est une association entre une clé (équivalente à un indice pour une liste) et une valeur. Alors que les indices d’une liste sont forcément les entiers compris entre 0 et la taille de la liste exclue, les clés d’un dictionnaire sont des valeurs quelconques, la seule contrainte étant qu’on ne peut pas avoir deux fois la même clé dans un dictionnaire. Notamment, ces clés ne sont pas nécessairement des entiers, on utilisera en effet souvent des dictionnaires lorsque l’on souhaite stocker des valeurs associées à des chaînes de caractères (qui seront les clés du dictionnaire).

Pour définir un dictionnaire par ses paires clé-valeur en Python, on peut utiliser la syntaxe suivante :

mon_dico = {"a" : 123, "z" : 7, "bbb" : None}
print(mon_dico)
# [Sortie] {'a': 123, 'bbb': None, 'z': 7}

On remarque ici que l’ordre dans lequel on a entré des paires clé-valeur n’est pas conservé lors de l’affichage.

6.1 Modification du contenu d’un dictionnaire

Pour modifier la valeur associée à une clé d’un dictionnaire, la syntaxe est similaire à celle utilisée pour les listes, en remplaçant les indices par les clés :

mon_dico = {"a" : 123, "z" : 7, "bbb" : None}
mon_dico["a"] = 1000
print(mon_dico)
# [Sortie] {'a': 1000, 'bbb': None, 'z': 7}

De même, on peut créer une nouvelle paire clé-valeur en utilisant la même syntaxe :

mon_dico = {"a" : 123, "z" : 7, "bbb" : None}
mon_dico["c"] = -1
print(mon_dico)
# [Sortie] {'c': -1, 'a': 123, 'bbb': None, 'z': 7}

Enfin, pour supprimer une paire clé-valeur d’un dictionnaire, on utilise le mot-clé del :

mon_dico = {"a" : 123, "z" : 7, "bbb" : None}
del mon_dico["a"]
print(mon_dico)
# [Sortie] {'bbb': None, 'z': 7}

6.2 Lecture du contenu d’un dictionnaire

Pour lire la valeur associée à une clé du dictionnaire, on peut utiliser la même syntaxe que pour les listes :

mon_dico = {"a" : 123, "z" : 7, "bbb" : None}
print(mon_dico["a"])
# [Sortie] 123

Par contre, si la clé demandée n’existe pas, cela génèrera une erreur. Pour éviter cela, on peut utiliser la méthode get qui permet de définir une valeur par défaut à retourner si la clé n’existe pas :

mon_dico = {"a" : 123, "z" : 7, "bbb" : None}
print(mon_dico.get("a", 0))
# [Sortie] 123
print(mon_dico.get("b", 0))
# [Sortie] 0

6.3 Parcours d’un dictionnaire

Pour parcourir le contenu d’un dictionnaire, il existe, comme pour les listes, trois possibilités.

6.3.1 Parcours par valeurs

Si l’on souhaite uniquement accéder aux valeurs stockées dans le dictionnaire, on utilisera la méthode values :

mon_dico = {"a" : 123, "z" : 7, "bbb" : None}
for val in mon_dico.values():
    print(val)
# [Sortie] 123
# [Sortie] None
# [Sortie] 7

6.3.2 Parcours par clés

Si l’on souhaite uniquement accéder aux clés stockées dans le dictionnaire, on utilisera la méthode keys :

mon_dico = {"a" : 123, "z" : 7, "bbb" : None}
for cle in mon_dico.keys():
    print(cle)
# [Sortie] a
# [Sortie] bbb
# [Sortie] z

6.3.3 Parcours par couples clés/valeurs

Si l’on souhaite accéder simultanément aux clés stockées dans le dictionnaire et aux valeurs associées, on utilisera la méthode items :

mon_dico = {"a" : 123, "z" : 7, "bbb" : None}
for cle, valeur in mon_dico.items():
    print(cle, valeur)
# [Sortie] a 123
# [Sortie] bbb None
# [Sortie] z 7

Exercice 6.1 Écrivez une fonction qui compte le nombre d’occurrences de chacun des mots d’une chaîne de caractères et retourne le résultat sous forme de dictionnaire :

# [...]
print(compte_occurrences("la vie est belle c'est la vie"))
# [Sortie] {"c'est": 1, 'la': 2, 'belle': 1, 'est': 1, 'vie': 2}

Exercice 6.2 Écrivez une fonction qui retourne la somme des valeurs d’un dictionnaire fourni en argument.

7 Lecture et écriture de fichiers textuels

Dans ce chapitre, nous nous intéressons à la lecture/écriture de fichiers textuels par un programme Python. Un premier élément qu’il est nécessaire de maîtriser pour lire ou écrire des fichiers textuels est la notion d’encodage. Il faut savoir qu’il existe plusieurs façons d’encoder un texte. Nous nous focaliserons ici sur les deux encodages que vous êtes les plus susceptibles de rencontrer (mais sachez qu’il en existe bien d’autres) :

La principale différence entre ces deux encodage réside dans leur façon de coder les accents. Ainsi, si le texte que vous lisez/écrivez ne contient aucun accent ou caractère spécial, il est probable que la question de l’encodage ne soit pas problématique dans votre cas. Au contraire, s’il est possible que vous utilisiez de tels caractères, il faudra bien faire attention à l’encodage utilisé, que vous spécifierez à l’ouverture du fichier. Si votre programme doit lire un fichier, il faudra donc vous assurer de l’encodage associé à ce fichier (en l’ouvrant par exemple avec un éditeur de texte qui soit suffisamment avancé pour vous fournir cette information). Si vous écrivez un programme qui écrit un fichier, il faudra vous poser la question de l’utilisation future qui sera faite de ce fichier : s’il est amené à être ouvert par un autre utilisateur, il serait pertinent de vous demander quel encodage sera le moins problématique pour cet utilisateur, par exemple.

Si vous n’avez pas de contrainte extérieure pour ce qui est de l’encodage, vous utiliserez l’encodage UTF-8 par défaut.

7.1 Lecture de fichiers textuels

Ce que nous appelons lecture de fichiers textuels en Python consiste à copier le contenu d’un fichier dans une (ou plusieurs) chaîne(s) de caractères. Cela implique deux étapes en Python :

  1. ouvrir le fichier en lecture ;
  2. parcourir le contenu du fichier.

La première étape d’ouverture du fichier en lecture est commune à tous les types de fichiers textuels. En supposant que le nom du fichier à ouvrir soit stocké sous forme de chaîne de caractères dans la variable nom_fichier, le code suivant ouvre un fichier en lecture avec l’encodage UTF-8 et stocke dans la variable fp un pointeur sur l’endroit où nous sommes rendus dans notre lecture du fichier (pour l’instant, le début du fichier) :

fp = open(nom_fichier, "r", encoding="utf-8")

Le second argument ("r") indique que le fichier doit être ouvert en mode read, donc en lecture.

7.1.1 Fichiers textuels génériques

Une fois le fichier ouvert en lecture, on peut le lire ligne par ligne à l’aide de la boucle suivante :

fp = open(nom_fichier, "r", encoding="utf-8")
for ligne in fp.readlines():
    print(ligne)

Ici, la variable ligne, de type chaîne de caractères, contiendra successivement le texte de chacune des lignes du fichier considéré.

7.1.2 Fichiers Comma-Separated Values (CSV)

Les fichiers Comma-Separated Values (CSV) permettent de stocker des données organisées sous la forme de tableaux dans des fichiers textuels. À l’origine, ces fichiers étaient organisées par ligne et au sein de chaque ligne les cellules du tableau (correspondant aux différentes colonnes) étaient séparées par des virgules (d’où le nom de ce type de fichiers). Aujourd’hui, la définition de ce format (lien) est plus générale que cela et différents délimiteurs sont acceptés. Pour manipuler ces fichiers, il existe en Python un module dédié, appelé csv. Ce module contient notamment une fonction reader permettant de simplifier la lecture de fichiers CSV. La syntaxe d’utilisation de cette fonction est la suivante (vous remarquerez la présence de l’attribut delimiter) :

import csv

nom_fichier = "..." # À remplacer par le chemin vers le fichier :)

# Contenu supposé du fichier :
# 1;2;3
# a;b

fp = open(nom_fichier, "r", encoding="utf-8")
for ligne in csv.reader(fp, delimiter=";"):
    for cellule in ligne:
        print(cellule)
    print("Fin de ligne")
# [Sortie] 1
# [Sortie] 2
# [Sortie] 3
# [Sortie] Fin de ligne
# [Sortie] a
# [Sortie] b
# [Sortie] Fin de ligne

On remarque ici que, contrairement au cas de fichiers textuels génériques, la variable de boucle ligne n’est plus une chaîne de caractères mais une liste de chaînes de caractères. Les éléments de cette liste sont les cellules du tableau représenté par le fichier CSV.

7.1.2.1 Cas des fichiers à en-tête

Souvent, les fichiers CSV comprennent une première ligne d’en-tête, comme dans l’exemple suivant :

NOM;PRENOM;AGE
Lemarchand;John;23
Trias;Anne;

Si l’on souhaite que, lors de la lecture du fichier CSV, chaque ligne soit représentée par un dictionnaire dont les clés sont les noms de colonnes (lus dans l’en-tête) et les valeurs associées sont celles lues dans la ligne courante, on utilisera csv.DictReader au lieu de csv.reader :

import csv

nom_fichier = "..." # À remplacer par le chemin vers le fichier :)

# Contenu supposé du fichier :
# NOM;PRENOM;AGE
# Lemarchand;John;23
# Trias;Anne;

fp = open(nom_fichier, "r", encoding="utf-8")
for ligne in csv.DictReader(fp, delimiter=";"):
    for cle, valeur in ligne.items():
        print(cle, valeur)
    print("--Fin de ligne--")
# [Sortie] AGE 23
# [Sortie] NOM Lemarchand
# [Sortie] PRENOM John
# [Sortie] --Fin de ligne--
# [Sortie] AGE
# [Sortie] NOM Trias
# [Sortie] PRENOM Anne
# [Sortie] --Fin de ligne--

7.1.2.2 Un peu de magie…

Dans certains cas, on ne sait pas à l’avance quel délimiteur est utilisé pour le fichier CSV à lire. On peut demander au module CSV de deviner le dialecte1 d’un fichier en lisant le début de ce fichier. Dans ce cas, la lecture du fichier se fera en 4 étapes :

  1. Ouverture du fichier en lecture ;
  2. Lecture des n premiers caractères du fichier pour tenter de deviner son dialecte ;
  3. “Rembobinage” du fichier pour recommencer la lecture au début ;
  4. Lecture du fichier en utilisant le dialecte détecté à l’étape 2.

Le choix du paramètre n doit être un compromis : il faut lire suffisamment de caractères pour que la détection de dialecte soit fiable, tout en sachant que lire beaucoup de caractères prendra du temps. En pratique, lire les 1000 premiers caractères d’un fichier est souvent suffisant pour déterminer son dialecte.

On obtient alors une syntaxe du type :

import csv

nom_fichier = "..." # À remplacer par le chemin vers le fichier :)

# Contenu supposé du fichier :
# 1,2,3
# a,b

fp = open(nom_fichier, "r", encoding="utf-8")  # Étape 1.
dialecte = csv.Sniffer().sniff(fp.read(1000))  # Étape 2.
fp.seek(0)                                     # Étape 3. À ne pas oublier !
for ligne in csv.reader(fp, dialect=dialecte): # Étape 4.
    for cellule in ligne:
        print(cellule)
    print("Fin de ligne")
# [Sortie] 1
# [Sortie] 2
# [Sortie] 3
# [Sortie] Fin de ligne
# [Sortie] a
# [Sortie] b
# [Sortie] Fin de ligne

7.1.3 Fichiers JavaScript Object Notation (JSON)

Les fichiers JavaScript Object Notation (JSON) permettent de stocker des données structurées (par exemple avec une organisation hiérarchique). Un document JSON s’apparente à un dictionnaire en Python (à la nuance près que les clés d’un document JSON sont forcément des chaînes de caractères). Voici un exemple de document JSON :

{
    "num_etudiant": "21300000",
    "notes": [12, 5, 14],
    "date_de_naissance": {
        "jour": 1,
        "mois": 1,
        "annee": 1995
    }
}

En Python, pour lire de tels fichiers, on dispose du module json qui contient une fonction load :

import json

nom_fichier = "..." # À remplacer par le chemin vers le fichier :)

# Contenu supposé du fichier :
# {
#    "num_etudiant": "21300000",
#    "notes": [12, 5, 14],
#    "date_de_naissance": {
#        "jour": 1,
#        "mois": 1,
#        "annee": 1995
#    }
# }

fp = open(nom_fichier, "r", encoding="utf-8")
d = json.load(fp)
print(d)
# [Sortie] {"notes": [12, 5, 14], "date_de_naissance":
# [Suite ]  {"jour": 1, "annee": 1995, "mois": 1},
# [Suite ]  "num_etudiant": "21300000"}

Il est à noter qu’un fichier JSON peut également contenir une liste de dictionnaires, comme dans l’exemple suivant :

[{
    "num_etudiant": "21300000",
    "notes": [12, 5, 14],
    "date_de_naissance": {
        "jour": 1,
        "mois": 1,
        "annee": 1995
    }
},
{
    "num_etudiant": "21300001",
    "notes": [14],
    "date_de_naissance": {
        "jour": 1,
        "mois": 6,
        "annee": 1989
    }
}]

Dans ce cas, json.load retournera une liste de dictionnaires au lieu d’un dictionnaire, bien évidemment.

Enfin, si l’on a stocké dans une variable une chaîne de caractères dont le contenu correspond à un document JSON, on peut également la transformer en dictionnaire (ou en liste de dictionnaires) à l’aide de la fonction json.loads (attention au “s” final) :

ch = '{"num_etudiant": "21300000",  "notes": [12, 5, 14]}'
d = json.loads(ch)  # loads : load (from) string
print(d)
# [Sortie] {"notes": [12, 5, 14], "num_etudiant": "21300000"}

7.2 Écriture de fichiers textuels

Ce que nous apellons écriture de fichiers textuels en Python consiste à copier le contenu d’une (ou plusieurs) chaîne(s) de caractères dans un fichier. Cela implique trois étapes en Python :

  1. ouvrir le fichier en écriture ;
  2. ajouter du contenu dans le fichier ;
  3. fermer le fichier.

La première étape d’ouverture du fichier en écriture est commune à tous les types de fichiers textuels. En supposant que le nom du fichier à ouvrir est stocké sous forme de chaîne de caractères dans la variable nom_fichier, le code suivant ouvre un fichier en écriture avec l’encodage UTF-8 et stocke dans la variable fp un pointeur sur l’endroit où nous sommes rendus dans notre écriture du fichier (pour l’instant, le début du fichier) :

fp = open(nom_fichier, "w", encoding="utf-8", newline="\n")

Le second argument ("w") indique que le fichier doit être ouvert en mode write, donc en écriture.

Si le fichier en question existait déjà, son contenu est tout d’abord écrasé et on repart d’un fichier vide. Si l’on souhaite au contraire ajouter du texte à la fin d’un fichier existant, on utilisera le mode append, symbolisé par la lettre "a" :

fp = open(nom_fichier, "a", encoding="utf-8", newline="\n")

Une fois les instructions d’écriture exécutées (voir plus bas), on doit fermer le fichier pour s’assurer que l’écriture sera effective :

fp.close()

Il est à noter que l’on peut, dans certains cas, se dispenser de fermer explicitement le fichier. Par exemple, si notre code est inclus dans un script Python, dès la fin de l’exécution du script, tous les fichiers ouverts en écriture par le script sont automatiquement fermés.

7.2.1 Fichiers textuels génériques

Pour ajouter du contenu à un fichier pointé par la variable fp, il suffit ensuite d’utiliser la méthode write :

fp.write("La vie est belle\n")

Notez que, contrairement à la fonction print à laquelle vous êtes habitué, la méthode write ne rajoute pas de caractère de fin de ligne après la chaîne de caractères passée en argument, il faut donc inclure ce caractère "\n" à la fin de la chaîne de caractères passée en argument, si vous souhaitez inclure un retour à la ligne.

7.2.2 Fichiers CSV

Le module csv déjà cité plus haut contient également une fonction writer permettant de simplifier l’écriture de fichiers CSV. La syntaxe d’utilisation de cette fonction est la suivante :

import csv

nom_fichier = "..." # À remplacer par le chemin vers le fichier :)

fp = open(nom_fichier, "w", encoding="utf-8", newline="\n")
csvfp = csv.writer(fp, delimiter=";")
csvfp.writerow([1, 5, 7])
csvfp.writerow([2, 3])
fp.close()
# Après cela, le fichier contiendra les lignes suivantes :
# 1;5;7
# 2;3

La méthode writerow prend donc une liste en argument et écrit dans le fichier les éléments de cette liste, séparés par le délimiteur ";" spécifié lors de l’appel à la fonction writer. Le retour à la ligne est écrit directement par la méthode writerow, vous n’avez pas à vous en occuper.

7.3 Manipulation de fichiers en Python avec le module os

Lorsque l’on lit ou écrit des fichiers, il est fréquent de vouloir répéter la même opération sur plusieurs fichiers, par exemple sur tous les fichiers avec l’extension ".txt" d’un répertoire donné. Pour ce faire, on peut utiliser en Python le module os qui propose un certain nombre de fonctions standard de manipulation de fichiers. On utilisera notamment la fonction listdir de ce module qui permet de lister l’ensemble des fichiers et sous-répertoires contenus dans un répertoire donné :

import os

for nom_fichier in os.listdir("donnees"):
    print(nom_fichier)

La fonction listdir peut prendre indifféremment un chemin absolu ou relatif (dans notre exemple, il s’agit d’un chemin relatif qui pointe sur le sous-répertoire "donnees" contenu dans le répertoire de travail courant du programme).

Si vous exécutez le code ci-dessus et que votre répertoire "donnees" n’est pas vide, vous remarquerez que le nom du fichier stocké dans la variable nom_fichier ne contient pas le chemin vers ce fichier. Or, si l’on souhaite ensuite ouvrir ce fichier (que ce soit en lecture ou en écriture), il faudra bien spécifier ce chemin. Pour cela, on utilisera la syntaxe suivante :

import os

repertoire = "donnees"
for nom_fichier in os.listdir(repertoire):
    nom_complet_fichier = os.path.join(repertoire, nom_fichier)
    print(nom_fichier)
    print(nom_complet_fichier)
    fp = open(nom_complet_fichier, "r", encoding="utf-8")
    # [...]

La fonction path.join du module os permet d’obtenir le chemin complet vers le fichier à partir du nom du répertoire dans lequel il se trouve et du nom du fichier isolé. Il est préférable d’utiliser cette fonction plutôt que d’effectuer la concaténation des chaînes de caractères correspondantes car la forme des chemins complets dépend du système d’exploitation utilisé, ce que gère intelligemment path.join.

Exercice 7.1 Écrivez une fonction qui affiche, pour chaque fichier d’extension ".txt" d’un répertoire passé en argument, le nom du fichier ainsi que son nombre de lignes.

Exercice 7.2 Écrivez une fonction qui retourne le nombre de fichiers présents dans un répertoire dont le nom est passé en argument. Vous pourrez vous aider pour cela de la documentation du sous-module path du module os (lien).

8 Récupération de données à partir d’API web

De nombreux services web fournissent des API (Application Programming Interface) pour mettre des données à disposition du grand public. Le principe de fonctionnement de ces API est le suivant : l’utilisateur effectue une requête sous la forme d’une requête HTTP, le service web met en forme les données correspondant à la requête et les renvoie à l’utilisateur, dans un format défini à l’avance.

Voici une liste (très loin d’être exhaustive) d’API web d’accès aux données :

Pour manipuler en Python de telles données, il faudra donc être capable :

  1. d’envoyer une requête HTTP et de récupérer le résultat ;
  2. de transformer le résultat en une variable Python facilement manipulable.

Pour ce qui est du second point, la plupart des API web offrent la possibilité de récupérer les données au format JSON. Nous avons vu précédemment dans ce cours que ce format était facilement manipulable en Python, notamment parce qu’il est très proche de la notion de dictionnaire. Ce chapitre se focalise donc sur la réalisation de requêtes HTTP en Python.

8.1 Requêtes HTTP en Python

8.1.1 Format d’une requête HTTP

Dans un premier temps, étudions le format d’une requête HTTP, telle que vous en effectuez des dizaines chaque jour, par l’intermédiaire de votre navigateur web. Lorsque vous entrez dans la barre d’adresse de votre navigateur l’URL suivante :

http://people.irisa.fr/Romain.Tavenard/index.php?page=3

votre navigateur va envoyer une requête au serveur concerné (cette requête ne contiendra pas uniquement l’URL visée mais aussi d’autres informations sur lesquelles nous ne nous attarderons pas ici). Dans l’URL précédente, on distingue 4 sous parties :

De la même façon, lors d’un appel à une API web, on spécifiera le protocole à utiliser, la machine à contacter, le chemin vers la ressource voulue et un certain nombre de paramètres qui décriront notre requête. Voici un exemple de requête à une API web (l’API Google Maps Directions en l’occurrence) :

https://maps.googleapis.com/maps/api/directions/json?origin=Toronto&destination=Montreal

Vous pouvez copier/coller cette URL dans la barre d’adresse de votre navigateur et observer ce que vous obtenez en retour. Observez que le résultat de cette requête est au format JSON. En fait, si vous étudiez plus précisément l’URL fournie, vous verrez que c’est nous qui avons demandé à obtenir le résultat dans ce format. De plus, on a spécifié dans l’URL que l’on souhaitait obtenir les informations d’itinéraire pour aller de Toronto (paramètre origin) à Montreal (paramètre destination).

En plus de ces paramètres, il est souvent utile (pour pouvoir profiter pleinement des fonctionnalités des API) de spécifier une clé d’API sous la forme d’un paramètre supplémentaire (nommé key dans les API Google Maps par exemple). Ainsi, la requête précédente deviendrait :

https://maps.googleapis.com/maps/api/directions/json?origin=Toronto&destination=Montreal&key=VOTRE_CLE

dans laquelle vous devrez remplacer VOTRE_CLE par une clé que vous aurez préalablement générée et qui vous permettra d’utiliser le service web de manière authentifiée.

8.1.2 Utilisation du module urllib.request

La section précédente proposait un rappel sur le format des requêtes HTTP et vous avez été invités à effectuer des requêtes HTTP à l’aide de votre navigateur. Si maintenant on souhaite récupérer de manière automatique le résultat d’une requête HTTP pour le manipuler en Python, le plus commode est d’effectuer la requête HTTP depuis Python. Pour cela, on utilise le module urllib.request. Ce module contient notamment une fonction urlopen qui se comporte (presque) comme la fonction open que vous connaissez qui permet de lire le contenu d’un fichier stocké sur votre disque dur :

import urllib.request

url = "..."  # Stockez ici votre requête HTTP

fp = urllib.request.urlopen(url)
contenu = fp.read().decode("utf-8")

On voit ici deux nuances par rapport à l’utilisation de la fonction open. Tout d’abord, il n’existe pas de fonction readlines permettant de lire le résultat de la requête HTTP ligne par ligne, on utilisera donc la fonction read. De toute façon, pour le cas de la lecture d’un résultat au format JSON, il n’aurait pas été pertinent de lire le résultat ligne par ligne car il faut avoir accès à l’ensemble du document JSON pour pouvoir le traiter avec le module json. Ensuite, on ne peut pas définir l’encodage à utiliser pour la lecture lors de l’appel à urlopen. On le fait donc dans un deuxième temps à l’aide de la méthode decode.

Une fois ces quelques lignes exécutées, la variable contenu contient une chaîne de caractères correspondant au document JSON retourné par l’API. Il suffit donc alors d’utiliser le module json pour transformer cette chaîne de caractères en données manipulables en Python.

En pratique, dans de nombreux cas, des modules Python existent pour permettre d’utiliser les API grand public sans avoir à gérer les requêtes HTTP directement. C’est par exemple le cas des modules googlemaps (qui permet d’accéder à toutes les API Google Maps citées plus haut) ou tweepy (pour l’API Twitter).

Exercice 8.1 Écrivez une fonction qui prenne en entrée une clé d’API Google Maps et deux villes et retourne le temps de trajet (en secondes) prévu par Google Maps API pour aller d’une ville à l’autre.

9 Tester son code

Dans ce document, nous avons jusqu’à présent supposé que tout se passait bien, que votre code ne retournait jamais d’erreur et qu’il ne contenait jamais de bug. Quel que soit votre niveau d’expertise en Python, ces deux hypothèses sont peu réalistes. Nous allons donc nous intéresser maintenant aux moyens de vérifier si votre code fait bien ce qu’on attend de lui et de mieux comprendre son comportement lorsque ce n’est pas le cas.

9.1 Les erreurs en Python

Étudions ce qu’il se passe lors de l’exécution du code suivant :

x = "12"
y = x + 2

Nous obtenons la sortie suivante :

Traceback (most recent call last):
  File "[...]", line 2, in <module>
    y = x + 2
TypeError: Can't convert 'int' object to str implicitly

Ce type de message d’erreur ne doit pas vous effrayer, il est là pour vous aider. Il vous fournit de précieuses informations :

  1. l’erreur se produit à la ligne 2 de votre script Python ;
  2. le problème est que Python ne peut pas convertir un objet de type int en chaîne de caractères (str) de manière implicite.

Reste à se demander pourquoi, dans le cas présent, Python voudrait transformer un entier en chaîne de caractères. Pour le comprendre, rendons-nous à la ligne 2 de notre script et décortiquons-la. Dans cette ligne (y = x + 2), deux opérations sont effectuées :

Nous avons vu dans ce document que Python savait effectuer l’opération + avec des opérandes de types variés (nombre + nombre, liste + liste, chaîne de caractères + chaîne de caractères, et il en existe d’autres). Intéressons-nous ici au type des opérandes considérées. La variable x telle que définie à la ligne 1 est de type chaîne de caractères. La valeur 2 est de type entier. Il se trouve que Python n’a pas défini d’addition entre chaîne de caractères et entier et c’est pour cela que l’on obtient une erreur. Plus précisément, l’interpréteur Python nous dit : “si je pouvais convertir la valeur entière en chaîne de caractères à la volée, je pourrais faire l’opération + qui serait alors une concaténation, mais je ne me permets pas de le faire tant que vous ne l’avez pas écrit de manière explicite”.

Maintenant que nous avons compris le sens de ce bug, il nous reste à le corriger. Si nous souhaitons faire la somme du nombre 12 (stocké sous forme de chaîne de caractères dans la variable x) et de la valeur 2, nous écrivons :

x = "12"
y = int(x) + 2

et l’addition s’effectue alors correctement entre deux valeurs numériques.

9.2 Les tests unitaires

Pour pouvoir être sûr du code que vous écrivez, il faut l’avoir testé sur un ensemble d’exemples qui vous semble refléter l’ensemble des cas de figures auxquels votre programme pourra être confronté. Or, cela représente un nombre de cas de figures très important dès lors que l’on commence à écrire des programmes un tant soit peu complexes. Ainsi, il est hautement recommandé de découper son code en fonctions de tailles raisonnables et qui puissent être testées indépendamment. Les tests associés à chacune de ces fonctions sont appelés tests unitaires.

Tout d’abord, en mettant en place de tels tests, vous pourrez détecter rapidement un éventuel bug dans votre code et ainsi gagner beaucoup de temps de développement. De plus, vous pourrez également vous assurer que les modifications ultérieures de votre code ne modifient pas son comportement pour les cas testés. En effet, lorsque l’on ajoute une fonctionnalité à un programme informatique, il faut avant toute choses s’assurer que celle-ci ne cassera pas le bon fonctionnement du programme dans les cas classiques d’utilisation pour lesquels il avait été à l’origine conçu.

Prenons maintenant un exemple concret. Supposons que l’on souhaite écrire une fonction bissextile capable de dire si une année est bissextile ou non. En se renseignant sur le sujet, on apprend qu’une année est bissextile si :

On en déduit un ensemble de tests adaptés :

print(bissextile(2004))  # True car divisible par 4 et non par 100
print(bissextile(1900))  # False car divisible par 100 et non par 400
print(bissextile(2000))  # True car divisible par 400
print(bissextile(1999))  # False car divisible ni par 4 ni par 100

On peut alors vérifier que le comportement de notre fonction bissextile est bien conforme à ce qui est attendu.

9.3 Le développement piloté par les tests

Le développement piloté par les tests (ou Test-Driven Development) est une technique de programmation qui consiste à rédiger les tests unitaires de votre programme avant même de rédiger le programme lui-même.

L’intérêt de cette façon de faire est qu’elle vous obligera à réfléchir aux différents cas d’utilisation d’une fonction avant de commencer à la coder. De plus, une fois ces différents cas identifiés, il est probable que la structure globale de la fonction à coder vous apparaisse plus clairement.

Si l’on reprend l’exemple de la fonction bissextile citée plus haut, on voit assez clairement qu’une fois que l’on a rédigé l’ensemble de tests, la fonction sera simple à coder et reprendra les différents cas considérés pour les tests:

def bissextile(annee):
    if annee % 4 == 0 and annee % 100 != 0:
        return True
    elif annee % 400 == 0:
        return True
    else:
        return False

Exercice 9.1 En utilisant les méthodes de développement préconisées dans ce chapitre, rédigez le code et les tests d’un programme permettant de déterminer le lendemain d’une date fournie sous la forme de trois entiers (jour, mois, année).

10 Conclusion

Dans ce document, nous avons abordé les principes de base de la programmation en Python, tels qu’enseignés à des étudiants non informaticiens de niveau Licence 2 (à l’Université de Rennes 2).

Comme indiqué en introduction, ce document se veut évolutif. N’hésitez donc pas à faire vos remarques à son auteur dont vous trouverez le contact sur sa page web.


  1. Le dialecte d’un fichier CSV définit, en fait, bien plus que le caractère de séparation des cellules, comme décrit dans ce document.