Fichiers texte (CSV) en python

1 – Introduction

Un fichier « CSV » est un fichier texte qui respecte un certain format. En effet, comme son nom l’indique (comma seperated values) : valeurs séparées par des virgules. Le format CSV est très utilisé comme format par les tableurs (comme excel) et les logiciels de base de données pour les opérations d’import et d’export.

Dans cet article, on va découvrir le format « CSV », puis étudier la manière de manipuler ce genre de fichiers en Python.

2 – Fichiers CSV

Le contenu d’un fichier est un ensemble de lignes, où chaque ligne est composée de plusieurs valeurs séparées par des virgules (la virgule est le séparateur par défaut).

Exemple de fichier CSV : 

Sexe,Prénom,Année de naissance
M,Bob,1990
F,Alice,1980
F,Eve,2000

Le séparateur par défaut est la virgule « , ». Toutefois, on peut définir d’autres séparateurs comme : le point-virgule « ; », l’espace  » « , les deux points « : », … etc.

3 – Python et fichiers CSV

Comme les fichiers CSV sont des fichiers texte, il faut d’abord les ouvrir en mode lecture texte « r ». Pour cela, on utilise la fonction « open »

file = open("fichier.csv", "r")

Ensuite, on peut commencer à lire le contenu. Il existe différentes méthodes pour lire un fichier CSV selon ce qu’on veut récupérer : quelques caractères, une lignes, la listes des lignes, ou tout simplement tout. Soit « file » notre variable python qui est obtenue après ouverture du fichier :

  • file.read(n) : lire les « n » (au max) premiers caractères du fichier et les renvoie dans une chaîne de caratères.
  • file.readline() : lire la première ligne, c’est-à-dire, lire des caractère jusqu’à arriver au caractère « \n ».
  • file.readlines() : lire tout le fichier et le retourner sous forme de liste de lignes.
  • file.read() : lire tout le contenu du fichier et le retourner dans une chaîne de caractères.

Pour écrire dans un fichier texte, on utilise la méthode « write ».

  • file.write(texte) : écrire « texte » dans le fichier.

Enfin, pour fermer le fichier, on utilise la méthode « close ». C’est toujours important de fermer un fichier surtout après une écriture.

  • file.close()

Par exemple, pour remplacer le séparateur d’un fichier CSV de virgule « , » à dière « # », on a deux manières de faire :

3.1 – Sans le module csv

Pour lire le contenu d’un fichier CSV sans utiliser le module Python « csv », il faudra lire le fichier ligne par ligne et parser les lignes en python :

f = open("fichier.csv", "r+")     # mode lecture/écriture
lignes = f.readlines()
f.seek(0) # retour au début du fichier
for l in lignes:
# découper les lignes en plusieurs éléments
s = l.split(",")
# regrouper les éléments
ss = "#".join(s)
# écrire la ligne dans le fichier
f.write(ss)
f.close()

3.2 – Avec le module csv

Avec le module CSV, on utilise une la fonction « reader » qui génère un itérateur et qui itère sur les ligne du fichier. Cette fonction s’occupe également de parser les lignes. Ensuite, on utilise « writer » pour l’écriture.

import csv
f = open("fichier.csv", "r+")
r = csv.reader(f, delimiter=",")
lignes = list(r)
f.seek(0) # retour au début du fichier
w = csv.writer(f, delimiter="#")
w.writerows(lignes)
f.close() # fermeture du fichier

3 – Quelques Exercices

D’autres exercices sont disponibles sur ma playlist youtube.

3.1 – Récupérer les commentaires d’un programme python

Hypothèse : Les chaînes de caractère dans le programme python ne contiennent pas le caractère « # ».

Les étapes à suivre sont :

  1. Ouverture du fichier en mode lecture « r »
  2. Pour chaque ligne :
    1. Vérifier si la ligne contient le caractère « # » et récupérer son indice.
    2. Extraire la sous chaîne de caractère à partir du « # » jusqu’à la fin de la ligne.
    3. mettre cette sous chaîne dans une liste.
  3. Fermeture du fichier et renvoi de la liste.

Le code de la fonction python ressemble à ça :

def getComments(file):
f = open(file) # ouverture du fichier en mode lecture
ligne = f.readline()
res = []
while ligne != "": # parcours du contenu du fichier
pos = ligne.find("#") # recherche l'indice de '#'
if pos != -1:
res.append(ligne[pos:])
ligne = f.readline()
f.close() # fermeture du fichier
return res # renvoi du résultat

3.2 – Récupérer une ligne d’un fichier CSV

On dispose d’un fichier CSV qui contient un certain nombre de lignes et de colonnes. On voudrait récupérer une ligne particulière. Le programme doit accepter les indices positifs et négatifs. Si l’indice de la ligne se trouve en dehors de l’intervalle des lignes du fichier, le programme renvoie une liste vide.

Les étapes à suivre sont :

  1. Ouverture du fichier en mode lecture.
  2. Lecture du contenu du fichier dans un reader CSV.
  3. Vérification que l’indice de ligne appartient bien à l’intervalle.
  4. Fermeture du fichier et renvoie de la ligne

Le programme ressemble à :

def getLigne(file, n, sep=","):
f = open(file, 'r')
# lire le contenu du fichier
r = csv.reader(f, delimiter=sep, quoting=csv.QUOTE_NONNUMERIC)
liste = list(r)
f.close()
if (n < len(liste)) and (n >= -len(liste)):
res = liste[n]
else:
res = []
return res

3.3 – Récupérer une colonne d’un fichier CSV

Cet exercice ressemble beaucoup à l’exercice précédent. Il suffit juste d’inverser les lignes et les colonnes du fichier pour retrouver exactement la même situation.

Les étapes à suivre sont :

  1. Ouverture du fichier et récupération du contenu dans un reader CSV.
  2. Inversement des lignes et des colonnes
  3. Suite comme l’exercice précédent.

Le programme ressemble à ça :

def getColonne(file, n, sep=","):
f = open(file, 'r')
r = csv.reader(f, delimiter=sep, quoting=csv.QUOTE_NONNUMERIC)
lr = list(zip(*r)) # inverser les lignes et les colonnes

# vérifier que "n" est dans le bon intervalle
if (n < len(lr)) and (n >= -len(lr)):
r = list(lr[n])
else:
r = []
f.close()
return r

Il existe une autre manière de faire : L’idée est de lire le contenu du fichier, de parcourir le contenu ligne par ligne et extraire l’élément en question de chaque ligne. Le code ressemble à :

def getColonne(file, n, sep=","):
f = open(file, 'r')
r = csv.reader(f, delimiter=sep, quoting=csv.QUOTE_NONNUMERIC)
liste = list(r)
f.close()
res = []
if (n < len(liste[0])) and (n >= -len(liste[0])):
for ligne in liste:
res.append(ligne[n])
return res

4 – Conclusion

Dans cet artcile, on a vu comment manipuler (ouverture/lecture/écriture) d’un fichier texte en Python, et particulièrement un fichier CSV.

L’article a traité également quelques exercices liés aux fichiers CSV. Vous pouvez trouvez plus d’exercices sur les fichiers CSV en Python sur ma playlist youtube

Tutoriel : Utilisation de base de OpenSSL

1 – Introduction

OpenSSL est une boite à outils qui comporte deux bibliothèques « libcrypto » et « libssl ». Elles implémentent respectivement une variété d’algorithmes de la cryptographie (Chiffrement symétrique/asymétrique, hachage, signature numérique, certificats, …) et le protocole de communication SSL/TLS. Elle offre également une interface ligne de commande qui s’appelle « openssl ».

Elle est développée en langage C, et est sortie en 1998 pour la première fois.

2 – Qu’est ce que OpenSSL

La bibliothèque OpenSSL est une implémentation libre des protocoles SSL et TSL. Elle donne accès à :

  • Une bibliothèque de fonctionnalité écrite en C permettant de réaliser des applications client/serveur sécurisées s’appuyant sur SSL/TSL,
  • Un ensemble d’exécutables en ligne de commande permettant :
    • La création de clés RSA, DSA (pour les signatures)
    • La création de certificat X509 (identification)
    • Le calcul d’empreinte (MD5, SHA, RIPEMD160, …)
    • Le chiffrement et le déchiffrement (RSA, DES, IDEA, RC2, RC4, Blowfish, …)
    • La réalisation de de tests de clients et serveurs SSL/TSL
    • La signature et le chiffrement de courriers (S/MIME).

Généralement, OpenSSL est installée par défaut sur les système d’exploitation Linux. Pour avoir l’aide sur les fonctionnalités et l’utilisation en général de la bibliothèque OpenSSL, il faut taper la commande :

$ man openssl

La syntaxe générale pour l’utilisation en mode shell des fonctionnalités OpenSSL est la suivante :

$ openssl <commande> <options>

3 – Utilisation

3.1 – Chiffrement symétrique

Le chiffrement symétrique ou crypto-système à clé privée, utilise la même clé pour le chiffrement et le déchiffrement. Ceci nécessite que l’émetteur et le récepteur du message partagent la même clé privée. La figure suivante illustre bien le principe du chiffrement symétrique :

Parmi les crypto-systèmes à clé privée, on trouve AES, BlowFish, … etc. 
Dans la suite, on va considérer le crypto-système AES. Pour utiliser AES, on a besoin d’un mot de passe avec lequel on génère une clé de chiffrement/déchiffrement. Pour cela, on peut choisir n’importe quel chaîne de caractères, ou on peut également générer aléatoirement une chaîne en utilisant la commande « rand » d’OpenSSL :

$ openssl rand 32 > aes.key 

Cette commande va générer aléatoirement une chaîne de « 32 » caractères et la stocke dans un fichier qui s’appelle « aes.key ».

Pour chiffrer un fichier « file.txt » par exemple avec le chiffrement AES et la clé « aes.key », on va utiliser la commande « enc » de « OpenSSL » :

$ openssl enc -aes-256-cbc -pbkdf2 -iter 100000 -salt -in file.txt -out file.aes - kfile aes.key

Quelques remarques :

  • -aes-256-cbc : Cette option indique que AES 256 avec le mode CDC qui est utilisé pour le chiffrement.
  • -pbkdf2 : Cette option d’indique l’utilisation de l’algorithme PBKDF2 avec un nombre d’itération (par défaut ou précisé par « -iter »), dans le but d’augmenter la résistance aux attaques par force brute.
  • -iter 100000 : Cette option sert à indiquer un nombre d’itérations différent de celui par défaut.
  • -salt : Cette option indique l’utilisation d’un salt lors du chiffrement.

Pour déchiffrer le message on utilise la même commande précédente à qui on ajoute l’option « -d » :

$ openssl enc -d -aes-256-cbc -pbkdf2 -iter 100000 -salt -in file.aes -out dec_file.txt - kfile aes.key

Cette commande va déchiffrer le fichier « file.aes » et mettre le résultat dans le fichier « dec_file.txt ».

3.2 – Chiffrement asymétrique

Les crypto-systèmes asymétriques utilisent une paire de clés (publique et privée). La clé publique est connue par tout le monde, et est utilisée pour chiffrer les messages destinés au propriétaire de la clé. La clé privée est utilisée, quant-à-elle, pour déchiffrer les messages reçus. La figure suivante illustre le principe de ce crypto-système :

3.2.1 – Génération de clés RSA

Avant de pouvoir chiffrer et déchiffrer des messages en utilisant RSA, il faudra d’abord créer une paire de clés. Pour cela, on utilise la commande « genrsa » de « OpenSSL » :

$ openssl genrsa -out key.pem 512

A la fin, on aura une clé privée RSA de 512 bits dans un fichier « key.pem ». 
Il est souvent recommander de protéger cette clé (fichier) par un mot de passe. Pour cela, l’algorithme DES3 est utilisé pour chiffrer ce fichier :

$ openssl rsa -in key.pem -des3 -out key.pem

A ce stade, on a un fichier « key.pem » qui contient une clé privée RSA chiffrer en utilisant un mot de passe et l’algorithme DES3. Ce fichier tel qu’il est ne sert pas à grand chose. En effet, on a besoin d’en extraire la clé publique et la partager avec les autres. Ceci va leur permettre de nous envoyer des messages chiffrés. Pour cela, il faut taper la commande suivante :

$ openssl rsa -in key.pem -pubout -out key.pub

Puisque, la clé privée « key.pem » est protéger par mot de passe, cette commande demande de taper le mot de passe, puis extrait la clé publique et la met dans un fichier « key.pub ». Ce dernier, peut être partagé avec les personnes qui veulent nous envoyer un message de manière confidentielle.

3.2.2 – Chiffrement avec RSA

Pour chiffrer un message avec RSA, on utilise la commande « rsault » avec l’option « -encrypt ». On lui donne la clé publique du destinataire « key.pub ». La commande pour faire le chiffrement est la suivante :

$ openssl rsautl -encrypt -pubin -inkey key.pub -in file.txt -out file.enc  

3.2.3 – Déchiffrement avec RSA

Pour déchiffrer un message avec RSA, on utilise la commande « rsault » avec l’option « -decrypt ». Il faut lui donner la clé privée comme paramètre :

$ openssl rsautl -decrypt -inkey key.pem -in file.enc -out file.dec

3.3 – Chiffrement hybride

Généralement le chiffrement asymétrique n’est pas utilisé tout seul, même s’il a des avantages. En effet, il est très souvent combiné avec le chiffrement symétrique Pour les raisons suivantes :

  •  Le chiffrement asymétrique (exemple RSA) ne permet pas de chiffrer de gros volumes de données. La taille des données qu’on peut chiffrer est limitée à quelques centaines d’octets.
  • Le chiffrement asymétrique est très gourmand en puissance de calcul contrairement au chiffrement symétrique.

Le chiffrement hybride profite des avantages des deux types de chiffrement (symétrique et asymétrique). En effet, le principe consiste à chiffrer les données avec une clé symétrique (AES par exemple) puis chiffrer la clé symétrique avec l’algortihme asymétrique. Ensuite il faudra envoyer les deux résultats (données chiffrées et clé chiffrée) au destinataire.

La figure suivante illustre ce principe :

3.4 – Signature numérique

La signature numérique sert à prouver qu’un message à été bel et bien envoyé par l’émetteur et que personne d’autre ne s’est fait passer pour lui. Pour cela, l’émetteur signe numériquement le message avant de l’envoyer. Et à la réception du message, le destinataire vérifie la signature numérique pour confirmer l’authenticité de l’émetteur. La figure suivante illustre ce principe :

Les crypto-systèmes à clé publique offrent le mécanisme de signature numérique contrairement aux crypto-systèmes à clé privée.

La signature numérique ne peut pas s’appliquer sur un gros volume de données (messages longs, fichiers volumineux, …), c’est pour cela qu’on signe le hash du message ou du fichier à envoyer. Les étapes à suivre sont :

  1. Calculer l’empreinte (ou le hash) du message :
    $ openssl dgst -sha256 file.aes > file.aes.hash
  2. Signer ce hash en utilisant le crypto-système à clé publique
    $ openssl rsautl -sign -inkey key.pem -in file.aes.hash -out file.signature
  3. Vérification de la signature : retrouver le hash à partir de la signature
    $ openssl rsautl -verify -pubin -inkey key.pub -in file.signature -out hash

4 – Conclusion

Dans cet article, on a présenté une introduction à la boite à outils OpenSSL et son utilisation pour faire du chiffrement symétrique et asymétrique. On a vu également la signature numérique avec un crypto-système à clé publique.

Les crypto-systèmes choisis sont RSA pour le chiffrement asymétrique et la signature numérique, et AES pour le chiffrement symétrique.

On a vu également un exemple d’utilisation de fonction de hashage : SHA256 pour la réalisation de la signature numérique.

Tutoriel : Utilisation de CP-ABE avec docker

1 – Introduction

Ciphertext-Policy Attibute-Based Encryption (CP-ABE) est une technique cryptographique très puissante, elle permet d’implémenter du contrôle d’accès basé sur des rôles (RBAC). Dans cet article, on va présenter le fonctionnement le CP-ABE en général, puis, on va utiliser la librairie openABE (Une bibliothèque qui implémente CP-ABE) pour tester CP-ABE.

Afin d’éviter d’installer OpenABE, on va utiliser une image docker toute prête pour ça.

2 – C’est quoi CP-ABE ?

CP-ABE pour « Ciphertext Policy Attibute-Based Encryption », est une technique cryptographique qui permet d’implémenter le contrôle d’accès basé sur les rôles. Les rôles sont appelés ici des attributs.

La figure 1 montre les phases d’initialisation et de génération de clés. Elles concernent toutes les deux l’autorité des attributs. Cette dernière initialise les paramètres du système en générant les clés maîtresse publique et secrète. Ces mêmes clés sont utilisées pour générer les clés utilisateurs en utilisant la liste des attributs de chaque utilisateur. 

La figure 2, quant à elle, illustre les phases de chiffrement de déchiffrement. Pour chiffrer un fichier, un utilisateur a besoin de la clé maîtresse publique, le fichier en question, et un politique d’accès (Elle est montrée sous forme d’arbre logique sur la figure). En suite, vient l’étape de déchiffrement, où l’utilisateur « bob » utilise sa propre clé secrète, qui contient des attributs satisfaisant la politique d’accès, pour retrouver le contenu du fichier original.

3 – Préparation du terrain

Nous allons préparer le terrain pour pouvoir utiliser la technique cryptographique CP-ABE.

3.1 – Installation Docker

Pour pouvoir continuer ce tutoriel, il faut installer docker. En effet, dans la suite, on va utiliser des containers docker pour lancer l’exécution des primitives CP-ABE. Ceci facilite énormément la tâche, puisqu’il n’est pas nécessaire d’installer OpenABE directement sur le système.

3.2 – Télécharger l’image docker

Pour télécharger l’image docker qui intègre la librairie OpenABE, il faut taper la commande :

$ docker pull touatily/openabe

L’image fait un peu plus de 2GB, donc ça prend un peu de temps pour qu’elle soit téléchargée. Vous pouvez vérifier que le téléchargement s’est bien passé :

$ docker images

Et là, vous devriez voir apparaître la ligne qui correspond à cette image docker :


touatily/openabe             latest        662030668539          33 hours ago       2.11GB

4 – Utilisation de CP-ABE

Il y a quatre commandes principales de la technique cryptographique CP-ABE : Initialisation, génération des clés utilisateurs, chiffrement, et déchiffrement.

4.1 – Initialisation

La phase d’initialisation sert à initialiser les paramètres du systèmes; plus exactement, les clés maîtresse secrète et publique de l’autorité des attributs sont créées. Pour cela on tape la commande :

$ docker run -it --rm -w /root -v$(pwd):/root touatily/openabe oabe_setup -s CP -p exemple

Cette commande crée deux fichiers dans le répertoire courant via le container docker (qui sera supprimé juste après l’exécution de la commande). Les deux fichiers correspondent aux clés maîtresse (secrète et publique).

$ ls
exemple.mpk.cpabe exemple.msk.cpabe

L’option « -p » permet juste d’ajouter un préfixe (« exemple » dans ce cas) aux deux fichiers qui stockent les clés secrète et publique de l’autorité d’attributs.

4.2 – Génération de clés utilisateurs

Une fois la phase d’initialisation terminée, on va utiliser les clés de l’autorité d’attributs pour créer les clés privées des utilisateurs.

Chaque utilisateur se voit accorder des attributs qui correspondent à ses rôles/privilèges dans l’organisation/institution. Par exemple, un étudiant « Bob » en 5ème année, en spécialité cyber-sécurité, dans un école d’ingénieur qui s’appelle ESME, aura la liste des attributs : « year=5 », « cybersecurite », « ESME ». Et si par exemple, l’étudiant fait partie d’une association de l’école qui s’appelle « BDE », on ajoute ce nom à sa liste d’attributs.

$ docker run -it --rm -w /root -v$(pwd):/root touatily/openabe oabe_keygen -s CP -p exemple -i "year=5|cybersecurite|ESME|BDE" -o bob.key

La commande génère la clé privée CP-ABE de « Bob » qui est liée à sa liste d’attributs « year=5, cyber-sécurité, ESME, BDE ». Cette clé est stockée dans le fichier « bob.key ».

Considérons un autre étudiant qui s’appelle « Alice ». Elle fait ses études dans la même école « ESME », elle est en 4ème année spécialité intelligence artificielle « IA ». Elle fait également partie de l’association « BDE ». Par conséquent, la liste des attributs de Alice est « year=4, IA, ESME, BDE ».

$ docker run -it --rm -w /root -v$(pwd):/root touatily/openabe oabe_keygen -s CP -p exemple -i "year=4|IA|ESME|BDE" -o alice.key

La commande génère un fichier « alice.key » qui contient la clé privée de l’étudiante « Alice ». Cette clé possède des informations sur la liste des attributs d’Alice.

4.3 – Chiffrement de fichiers

Dans cette étape, on va chiffrer trois fichiers différents avec trois politique d’accès différents. Il faut noter ici, que la politique d’accès est choisie selon le contenu du fichier, plus précisément, en se basant sur la liste des personnes autorisées à lire le fichier.

Ce tableau résume la situation :

DescriptionsPolitiques d’accès
fichier1Ce fichier est destinés aux étudiants de l’ESME en spécialité Cyber-sécurité« ESME AND cybersecurite »
fichier2C’est un fichier destiné aux étudiants de l’ESME qui sont en 4ème année.« ESME AND year==4 »
fichier3C’est un fichier destinié aux étudiants de l’ESME qui font partie de l’assciation BDE. Ces étudiants doivent être en 4ème année ou une classe supérieure.« ESME AND BDE AND year>=4 »

Pour chiffrer les trois fichiers précédents, il faut tapper les commandes :

$ docker run -i --rm -w /root -v$(pwd):/root touatily/openabe oabe_enc -s CP -e "ESME AND cybersecurite" -i fichier1 -o encrypted_fichier1 -p exemple

$ docker run -i --rm -w /root -v$(pwd):/root touatily/openabe oabe_enc -s CP -e "ESME AND year==4" -i fichier2 -o encrypted_fichier2 -p exemple

$ docker run -i --rm -w /root -v$(pwd):/root touatily/openabe oabe_enc -s CP -e "ESME AND BDE AND year>=4" -i fichier3 -o encrypted_fichier3 -p exemple

Ces trois commandes génèrent trois fichiers : « encrypted_fichier1.cpabe », « encrypted_fichier2.cpabe », et « encrypted_fichier3.cpabe » qui correspondent aux résultats de chiffrement des fichiers « fichier1 », « fichier2 », et « fichier3 » respectivement. Comme indiqué précédemment, chacun des trois fichiers est chiffré selon une politique d’accès propre à lui.

4.4 – Déchiffrement

Dans cette étape, les deux étudiants « Alice » et « Bob » vont essayer de déchiffrer les fichiers qui ont été chiffrés durant l’étape précédente.

4.4.1 – Alice

D’après les politiques d’accès utilisées pour chiffrer les trois fichiers, Alice devrait pouvoir déchiffrer seulement « fichier2 » et « fichier3 ».

  • Fichier 1:
$ docker run -it --rm -w /root -v$(pwd):/root touatily/openabe oabe_dec -s CP -k alice.key -i encrypted_fichier1.cpabe -o alice_decrypted_fichier1 -p exemple 

ciphertext: encrypted_fichier1.cpabe
user’s SK file: alice.key
abe/zcontextcca.cpp:decrypt:613: ‘Error occurred during decryption’
caught exception: Error occurred during decryption

Une erreur se produit car la clé d’Alice ne permet pas de déchiffrer le fichier1.

  • Fichier 2:
$ docker run -it --rm -w /root -v$(pwd):/root touatily/openabe oabe_dec -s CP -k alice.key -i encrypted_fichier2.cpabe -o alice_decrypted_fichier2 -p exemple

ciphertext: encrypted_fichier2.cpabe
user’s SK file: alice.key

Cette fois, Alice réussit à déchiffrer le fichier2. Le contenu du fichier « decrypted_fichier2 » est identique à celui du fichier « fichier2 ».

  • Fichier 3:
$ docker run -it --rm -w /root -v$(pwd):/root touatily/openabe oabe_dec -s CP -k alice.key -i encrypted_fichier3.cpabe -o alice_decrypted_fichier3 -p exemple

ciphertext: encrypted_fichier3.cpabe
user’s SK file: alice.key

Là encore, Alice réussit à déchiffrer le fichier et voir son contenu. Ce dernier est identique à celui de « fichier3 ».

4.4.2 – Bob

« Bob » de son côté tente de déchiffrer les trois fichiers. D’après les politiques d’accès et sa liste d’attributs, il devrait pouvoir déchiffrer « fichier1 » et « fichier3 », mais pas « fichier2 ».

  • Fichier 1:
$ docker run -it --rm -w /root -v$(pwd):/root touatily/openabe oabe_dec -s CP -k bob.key -i encrypted_fichier1.cpabe -o bob_decrypted_fichier1 -p exemple 

ciphertext: encrypted_fichier1.cpabe
user’s SK file: bob.key

Ceci va créer un fichier qui s’appelle « bob_decrypted_fichier1 », dont le contenu est identique à celui de « fichier1 ».

  • Fichier 2:
$ docker run -it --rm -w /root -v$(pwd):/root touatily/openabe oabe_dec -s CP -k bob.key -i encrypted_fichier2.cpabe -o alice_decrypted_fichier2 -p exemple

ciphertext: encrypted_fichier2.cpabe
user’s SK file: bob.key
abe/zcontextcca.cpp:decrypt:613: ‘Error occurred during decryption’
caught exception: Error occurred during decryption

Aucun fichier ne sera généré. Ceci est dû au fait que « bob » n’est pas en 4ème année (il ne possède pas l’attribut « year=4 »).

  • Fichier 3:
$ docker run -it --rm -w /root -v$(pwd):/root touatily/openabe oabe_dec -s CP -k bob.key -i encrypted_fichier3.cpabe -o bob_decrypted_fichier3 -p exemple

ciphertext: encrypted_fichier3.cpabe
user’s SK file: bob.key

Un fichier « bob_decrypted_fichier3 » est généré.

4.4.3 – Récapitulatif

Alice n’a pas pu déchiffrer le fichier « bob_decrypted_fichier3 » car elle n’est pas en spécialité cyber-sécurité (Elle ne possède pas l’attribut « cybersecurite »).
Bob, quant à lui, il n’a pas réussit à déchiffrer le fichier « encrypted_fichier2.cpabe » car il est en 5ème année, et que le fichier est destiné aux étudiant de 4ème année.

Remarques :

Qu’on a chiffré le fichier 3, on a utilisé la condition « year >= 4″ pour préciser que le fichier est destiné à la fois aux étudiants de 4ème et 5ème année. On aurait pu exprimer cette condition de la manière suivante  » year in (4-5) » ou encore « (year == 4) OR (year == 5) ». Ces deux dernière syntaxes sont plus précises que celle utilisée.

La librairie OpenABE ne supporte pas les caractères accentués. C’est pour cela qu’on a utilisé l’attribut « cybersecurite » et non « cybersécurité ».

La librairie OpenABE implémente également les techniques cryptographiques KP-ABE et IBE qui ne sont pas présentées ici.

5 – Conclusion

Dans cet article, nous avons présenté une technique cryptographique très puissante qui est CP-ABE. Elle permet d’implémenter du contrôle d’accès basé sur des rôles. « openABE » est une implémentation de cette technique cryptographique. on a utilisé un image docker pour exécuter les différentes commandes d’initialisation, génération de clés, et chiffrement/déchiffrement de fichiers.

Les conteneurs en Python : La classe range

1 – Introduction

Dans cet article de blog, on va découvrir la classe « range » et son utilisation en général. C’est une classe très utile surtout pour faire des boucles « for » avec un nombre d’itérations connu à l’avance, parcours de listes, de chaines de caractères, … etc.

2 – Utilisation

2.1 – Syntaxe

La classe range est utilisée quand on veut avoir un objet qui contient une suite de nombres entiers. Il existe trois syntaxes différentes pour créer un objet de type « range » :

>>> a1 = range(b)             # un seul paramètre
>>> a2 = range(a, b) # deux paramètres
>>> a3 = range(a, b, c) # trois paramètres
  • La syntaxe avec un seul paramètre crée un objet « a1 » de type « range » qui contient les éléments de « 0 » à « b-1 ». Si « b » est un nombre entier strictement positif, « a1 » contient exactement « b » éléments. Dans le cas contraire, « a1 » ne contient aucun élément. De manière générale :
range(b) --------> 0, 1, 2, ..., b-1
  • La deuxième syntaxe représente un forme un peu plus générique de la construction d’un objet « range ». En effet, avec cette syntaxe, on a la possibilité de préciser le début « a » de la suite de nombres. Dans ce cas, l’objet « a2 » contient les élément de « a » à « b-1 ». Si « b » est supérieur strictement à « a », l’objet « a2 » contient exactement « b-a » éléments, sinon (si « a » est supérieur ou égal à « b ») l’objet « a2 » ne contient aucun élément. De manière générale :
range(a, b) --------> a, a+1, a+2, ..., b-1
  • La troisième syntaxe est la forme la plus générique.  Dans ce cas là, trois paramètres sont à fournir qui correspondent respectivement au début de la suite de nombre, la borne supérieure, et le « pas ». Le « pas » est égal à la différence entre deux nombre successifs de cette suite de nombres. De manière générale :
range(a, b, c) --------> a, a+c, a+2*c, a+3*c, ..., a+k*c
tel que : a+k*c < b <= a+(k+1)*c

La figure suivante illustre le lien qui existe entre les trois syntaxes :

Figure 1: Equivalences entre les différentes syntaxes

2.2 – Cas d’utilisation

2.2.1 – Exemples

Généralement, on utilise « range » pour fixer le nombre d’itérations d’une boucle « for » (voir article sur les structures de contrôle).

for i in range(100):
print("Hello world!")

Le code précédent affiche cent (100) fois la phrase « Hello world! ».

2.3.2 – Méthodes d’un objet range

  • La méthode « index » :
    La méthode index prend un seul paramètre et retourne l’indice de ce paramètre dans l’objet. Si la valeur du paramètre n’appartient pas à l’objet « range », la méthode lève une exception de type « ValueError ».
>>> r = range(10)
>>> r.index(9)
9
>>> r.index(10)
Traceback (most recent call last):
File "<pyshell#2>", line 1, in <module>
r.index(10)
ValueError: 10 is not in range
  • La méthode « count » :
    La méthode « count », quant à elle, permet de compter le nombre d’apparitions d’une valeur dans l’ibjet range.
>>> r = range(1, 10, 2)
>>> r.count(1)
1
>>> r.count(10)
0

2.3.3 – Attributs d’un objet range

La classe « range » définit trois attributs : « start », « stop », « step ». Ils correspondent respectivement à la valeur de début, la borne sup, et le pas. Les trois attributs sont en lecture seule; C’est-à-dire, qu’une fois l’objet range est créé, on ne peut plus modifier les valeurs de ces attributs.

>>> r = range(1, 100, 3)
>>> r.start
1
>>> r.stop
100
>>> r.step
3
>>> r.start = 6
Traceback (most recent call last):
File "<pyshell#10>", line 1, in <module>
r.start = 6
AttributeError: readonly attribute

Voici comment sont intialisés les trois attributs selon la syntaxe utilisée lors de la création de l’objet (Voir section 2.1) :

startstopstep
range(b)0b1
range(a, b)ab1
range(a, b, c)abc

2.3.4 – Quelques astuces

  • Vérifier qu’une valeur appartient à un objet range :

Pour vérifier qu’une valeur « v » appartient à une objet de type range « r », on peut utiliser l’opérateur « in » :

>>> r = range(0,100, 2)
>>> v = 50
>>> v in r
True
>>> 100 in r
False

Bien sûr, on peut également utiliser le méthode « count » qui retourne « 1 » si la valeur appartient bien à l’objet range, « 0 » sinon.

  • Construire une liste à partir des éléments range :

Comme on vu précécemment, un objet range ne stocke pas toutes les valeurs mais consèrve seulement les informations sur le début, fin, et le pas concernant la suite de nombres. Pour avoir la liste complète des nombres à partir des éléments d’un objet range :

>>> r = range(2, 10, 2)  
>>> l1 = list(r) # première syntaxe
>>> l2 = [*r] # deuxième syntaxe
>>> l1
[2, 4, 6, 8]
>>> l2
[2, 4, 6, 8]
  • Itérer sur un objet range :

Comme un objet de type « range » est un itérable, on peut créer un itérateur sur cet objet comme suit :

>>> r = range(0,10,1)
>>> it = iter(r)
>>> next(it)
0
>>> next(it)
1
>>> next(it)
2

3 – Conclusion

Dans cet article, nous avons présenté la classe python « range » de manière générale, les différente manières de créer des objets « range », et comment les utiliser.

Tutoriel : Reverse-Proxy avec Docker

1 – Introduction

Dans ce tutoriel, on va voir comment mettre en place, étape par étape, un reverse-proxy dans un container docker. On va créer un container reverse-proxy et trois containers serveurs web qui, chacun, se trouve dans son propre réseau.

Le reverse-proxy va nous permettre de déployer plusieurs applications web (sites web) sur la même machine qui seront accessibles avec la même adresse IP. L’aiguillage des requêtes se fait selon le nom de domaine de chaque site par le reverse-proxy.

2 – Architecture générale

Le système qu’on veut mettre en place se compose d’un container reverse-proxy qui va recevoir toutes les requêtes HTTP, puis, en fonction du nom de domaine (dans l’url) le reverse-proxy aiguille ces requêtes vers les différents containers serveurs web.

Les containers serveur web peuvent être tous dans le même réseau comme ils peuvent aussi être dans des sous réseaux différents. Pour rester générique, on va considérer trois containers serveurs web se trouvant dans trois réseaux différents.

Pour que le containers reverse proxy puisse communiquer avec les containers serveurs web, il doit avoir une interface dans le même réseau que chacun d’eux en plus de son propore réseau.

Le tableau suivant résume les informations sur les différents containers :

Nom ContainerAdresse IPRéseauImage DockerURL
Serveur_web_1192.168.0.3192.168.0.0/24web-server (httpd)http://site1.com
Serveur_web_2192.168.1.3192.168.1.0/24web-server (httpd)http://site2.com
Serveur_web_3192.168.2.3192.168.2.0/24web-server (httpd)http://site3.com
Reverse-Proxy192.168.0.2
192.168.1.2
192.168.2.2
172.17.0.2
192.168.0.0/24
192.168.1.0/24
192.168.2.0/24
172.17.0.0/16
reverse-proxy (nginx)

La figure suivante illustre l’architecture générale de notre système :

3 – Préparation des images docker

Pour mettre en place notre architecture décrite précédemment, on a besoin de préparer les images docker afin de créer les containers. Plus précisément, on a besoin de deux images docker :

  • Une image docker pour le reverse-proxy basée sur « nginx » , et
  • Une autre image pour les serveurs web basée sur « httpd » .

3.1 – Solution 1 : avec des dockerfiles

Poour créer les images du reverse-proxy nginx et les serveurs web httpd en utilisant des dockerfiles, il faut copier ces deux textes suivants et les mettre dans deux fichiers, puis utiliser la commande « docker build » pour créer les deux images.

Le ficher « Dockerfile1 » pour le reverse-proxy nginx :

FROM nginx

RUN apt-get update
RUN apt-get install -y nano
RUN mkdir -p /etc/nginx/sites-enabled
RUN mkdir -p /etc/nginx/sites-available
ENTRYPOINT service nginx restart && /bin/bash

Le fichier « Dockerfile2 » pour le serveur web httpd :

FROM httpd
RUN apt-get update
RUN apt-get install -y nano

Maintenant que les deux fichiers dockerfile sont créés, il est possible de créer les deux images en faisant :

$ docker build -t touatily/reverse-proxy -f Dockerfile1 .
$ docker build -t touatily/web-server -f Dockerfile2 .

Les deux images s’appellent « touatily/reverse-proxy » et « touatily/web-server » respectivement. On peut vérifier ce en faisant :

$ docker images

3.2 – Solution 2 : téléchargement depuis dockerhub

La deuxième solution consiste à récupérer les images docker directement depuis docker hub. Pour cela, il faut taper les commandes suivantes :

$ docker pull touatily/reverse-proxy
$ docker pull touatily/web-server

Pour vérifier que les images ont bien été téléchargées depuis docker hub, il faut afficher la liste des images avec la commande « docker images » :

$ docker images

4 – Création des containers

À ce niveau, les images docker sont prêtes. Mais avant de créer les containers concrètement, il faut passer par plusieurs étapes :

4.1 – Préparation des réseaux

Comme indiqué précédemment, il nous faut un réseau par serveur web. Donc, en tout, on a besoin de trois réseaux en plus des réseaux créés par défaut par docker. Les informations sur les réseaux sont données plus haut dans le tableau récapitulatif.

Les commandes à exécuter sont les suivantes :

$ docker network create --subnet=192.168.0.0/24 --gateway=192.168.0.1 --driver=bridge réseau1
$ docker network create --subnet=192.168.1.0/24 --gateway=192.168.1.1 --driver=bridge réseau2
$ docker network create --subnet=192.168.2.0/24 --gateway=192.168.2.1 --driver=bridge réseau3

Pour vérifier que tout est en ordre, on peut afficher la liste des réseaux existants, et si tout s’est bien déroulé, on verra les trois réseaux (réseau1, réseau2, et réseau3) s’afficher en plus des trois réseaux créés par défaut :

docker network ls

4.2 – Lancement des containers

4.2.1 – Préparation des sites web

Avant de lancer les trois containers serveurs web, on crée trois répertoires (contenant chacun un fichier « index.html ») qui correspondent au trois sites web :

$ mkdir -p /root/websites/site1
$ echo "<h1>Site web 1</h1>" > /root/websites/site1/index.html
$ mkdir -p /root/websites/site2
$ echo "<h1>Site web 2</h1>" > /root/websites/site2/index.html
$ mkdir -p /root/websites/site3
$ echo "<h1>Site web 3</h1>" > /root/websites/site3/index.html

Evidemment, on peut mettre d’autres fichier HTML dans les trois répertoires, mais par soucis de simplicité, on se limite à des sites web constitués d’une seule page HTML.

4.2.2 – Création des containers serveurs web

Pour lancer les trois containers serveurs web il faut taper les commandes suivantes :

$ docker container run -itd --name serveur_web_1 --net réseau1 --ip 192.168.0.3 -v /root/websites/site1/:/usr/local/apache2/htdocs/  touatily/web-server 
$ docker container run -itd --name serveur_web_2 --net réseau2 --ip 192.168.1.3 -v /root/websites/site2/:/usr/local/apache2/htdocs/ touatily/web-server
$ docker container run -itd --name serveur_web_3 --net réseau3 --ip 192.168.2.3 -v /root/websites/site3/:/usr/local/apache2/htdocs/ touatily/web-server

Maintenant, il nous reste à créer le container pour le reverse-proxy nginx.

$ sudo docker container run -itd --name reverse-proxy -p80:80 -v /srv/virtual-hosts/:/etc/nginx/sites-available/ touatily/reverse-proxy

Quelques remarques s’imposent :

  • Il faut lancer le container reverse-proxy en utilisant sudo, car pour pouvoir mapper le port 80 de la machine, il faut les droits root.
  • Pour chacun des trois containers serveur web, on a mappé son repertoire « /usr/local/apache2/htdocs/ » sur le répertoire de la machine « /root/websites/site[123]/ ». Donc, pour mettre à jour le contenu des sites web, il suffit de modifier le contenu des répertoires de la machine « /root/websites/site[123]/ » et les changement seront pris en considération immédiatement.

4.3 – Configurations

4.3.1 – Côté reverse-proxy :

Il faudra créer les hôtes virtuels (un hôte virtuel par site web). Pour cela, dans le répertoire « /srv/virtual-hosts/ » qu’on a mappé sur le répertoire « /etc/nginx/sites-available/ » du container, on va créer un fichier de configuration par hôte virtuel (virtual host). Voici à quoi ressemble le fichier « site1.com » de configuration de l’hôte virtuel « site1.com » :

server {
listen 80;
server_name site1.com;

location / {
proxy_pass http://192.168.0.3;
}
}

Il faudra créer deux autres fichiers qui correspondent aux deux autres hôtes virtuels « site2.com » et « site3.com » en changeant le texte surligné en jaune par les valeurs « site2.com » et « http://192.168.1.3 » pour l’hôte virtuel « site2.com » et par les valeurs « site3.com » et « http://192.168.2.3 » pour l’hôte virtuel « site3.com ».

Maintenant, il faut s’assurer que le reverse-proxy nginx prenne en considération ces hôtes virtuels en vérifiant son fichier de configuration « /etc/nginx/nginx.conf ». Plus exactement, il faut vérifier que la ligne « include /etc/nginx/sites-enabled/*; » est présente dans la partie « http { … } » :

...
http {
...
include /etc/nginx/sites-enabled/*;
...
}

Pour ce, vous pouvez vous attachez au container puis modifier le fichier en faisant :

$ docker container attach reverse-proxy
root@f4be511eeda8:~# nano /etc/nginx/nginx.conf

Il nous reste encore à activer les différent hôtes virtuels et recharger la configuration pour que nginx prenne en considération des changement éffectués :

$ docker exec reverse-proxy ln -s /etc/nginx/sites-available/site1.com /etc/nginx/sites-enabled/site1.com
$ docker exec reverse-proxy ln -s /etc/nginx/sites-available/site2.com /etc/nginx/sites-enabled/site2.com
$ docker exec reverse-proxy ln -s /etc/nginx/sites-available/site3.com /etc/nginx/sites-enabled/site3.com

$ docker exec reverse service nginx reload

Reamrques :

  • Il ne faut pas redémarrer le service nginx sur le reverse proxy car cela va tuer le processus principal du container.
  • Comme on a mappé le répertoire des hôtes virtuels de nginx « /etc/nginx/sites-available/ » dans le répertoire « /srv/virtual-hosts/ », si jamais on a besoin d’ajouter de nouveaux hôtes virtuels, il suffit de
    • Ajouter le fichier de configuration dans « /srv/virtual-hosts/ »,
    • Activier l’hôte virtuel en créant un lien symbolique dans « /etc/nginx/sites-enabled/ », puis
    • Recharger les configurations nginx.

Il faudra maintenant ajouter notre container reverse proxy aux différents réseaux déjà créés, pour qu’il puisse communiquer avec les trois containers serveurs web :

$ docker network connect --ip 192.168.0.2 réseau1 reverse-proxy
$ docker network connect --ip 192.168.1.2 réseau2 reverse-proxy
$ docker network connect --ip 192.168.2.2 réseau3 reverse-proxy

4.3.2 – Côté serveurs web :

Il n’y a pas de configuration à faire du côté des serveurs web.

4.3.3 – Côté client :

Comme les trois noms de domaine « site1.com », « site2.com », et « site3.com » ne sont pas enregistrés dans un DNS, il faut les définir localement sur la machine locale. Pour cela, on n’a qu’à modifier le fichier « /etc/hosts » en ajoutant une ligne pour chaque nom de domaine.

172.17.0.2      site1.com
172.17.0.2 site2.com
172.17.0.2 site3.com

Il faut noter que l’adresse IP est la même pour les trois noms de domaine, et, est celle du reverse-proxy. En effet, toute requête demandant une url d’un des trois noms de domaine dois être routée vers le reverse-proxy. Et c’est ce dernier qui fait l’aiguillage.

5 – Tests & Vérifications

Pour vérifier que tout est bien mis en place, il suffit d’ouvrir une navigateur web et de saisir les url des trois sites web : « site1.com », « site2.com », et « site3.com ».

Les figures suivante montrent bien le résultat :

Il est possible également de faire la vérification en utilisant la commande « curl » :

$ curl site1.com
$ curl site2.com
$ curl site3.com

6 – Conclusion

Dans cette article, nous avons vu comment mettre en place un reverse-proxy et trois serveurs web en utilisant des containers docker. Le rôle du reverse-proxy et de recevoir toutes les requêtes web arrivant sur la machines, ensuite, il s’en charge de les aiguiller, selon le nom de domaine demandé, vers les différents serveurs web. Ces derniers peuvent se trouver dans différents réseaux.

Le reverse-proxy se charge également de relayer les réponses générer par les serveurs web aux clients qui ont émis les requêtes.

Les gestion des erreurs en Python : Les exceptions

1 – Introduction

Pour qu’un programme soit complet, il doit tenir compte de tous les cas possibles, en particulier, quand il manipule des données saisies par l’utilisateur. En effet, un programme qui demande à un utilisateur de saisir un entier par exemple, doit vérifier que la saisie est bien un entier et non autre chose (chaîne de caractère ou flottant par exemple).

Un autre exemple, c’est quand un programme utilise des fonctions systèmes de manipulation de fichiers, de communication réseau, … etc. Il doit toujours vérifier que l’appel système a bien réussi et n’a pas généré d’erreur.

Le mécanisme utilisé pour la gestion des erreurs en Python est : Les « exceptions ».

2 – Gestion des erreurs

Dans un programme, quand on a une partie du code qui est suceptible de générer une erreur et donc de lever une exception, on place cette partie du code dans un bloc « try … except … ».

Voici la syntaxe dans le cas général :

try:
<code susceptible de générer une exception>
except:
<traitement de l'exception>
[
finally:
<code exécuté dans tous les cas>
]

Dans le code précédent, le bloc « try » commence à s’exécuter. Si tout se passe bien, le bloc « expect » sera ignoré et on passe directement au bloc « finally ».
Si jamais une erreur se produit, l’exécution du bloc « try » est stoppée. Une exception, dont le type dépend de la nature de l’erreur, est levée. Cette dernière va être traitée par le bloc « except » prévu à cet effet. Celui-ci. Enfin, le bloc « finally » s’exécute. Remarquez que le bloc « finally » s’exécute dans tous les cas.

Il arrive également où une même partie du code soit susceptible de générer plusieurs types d’exception dans ce cas, on a la possibilité de mettre un seul bloc « except », et donc on définie un traitement commun à toutes les exceptions comme montré dans le code précédent.

La deuxième solution serait de mettre plusieurs blocs « except », et donc de définir plusieurs traitements selon le type de l’exception :

try:
<code susceptible de générer des exceptions>
except <exception 1>:
<traitement exception 1>
except <exception 2>:
<traitement exception 2>
...
except <exception n>:
<traitement exception n>
[
finally:
<traitement final>
]

Python définie une multitude de classes qui pourraient être utilisées pour déclencher des exceptions. La liste exhaustive de la hiérarchie des exceptions se trouve ici.

3 – Créer de nouveaux types d’exceptions

Python offre la possibilité de créer de nouveaux types d’exception. Pour ce, il faut créer une classe qui hérite, de manière directe ou indirecte, de la classe « BaseException » (La classe « BaseEception » doit être une une classe ancêtre de la classe à définir).

L’exemple suivant montre la définition d’une classe « ErreurPersonnalisee » qui hérite de la classe « BaseException ». Elle définie deux attributs : « code » et « msg » :

class ErreurPersonnalisee (BaseException):
def __init__(self, msg, code):
super().__init__(msg)
self.msg = msg
self.code = code
def __str__(self):
return str(self.code) + " - " + str(self.msg) + "\n"

raise ErreurPersonnalisee("Informations sur l'erreur", 1)

L’affichage produit est :

Traceback (most recent call last):
    File "test.py", line 10, in <module>
       raise erreur("Informations sur l'erreur", 1)
__main__.erreur: 1 - Informations sur l'erreur

4 – Exemples

4.1 – Lecture d’un fichier json

Les erreurs qui peuvent se produisent quand on lit un fichier JSON sont :

  • Le fichier est inexistant (L’exception levée dans ce cas est : « FileNotFoundError »),
  • Le fichier existe mais le programme n’a pas de droit de lecture sur lui (l’exception est « PermissionError »),
  • Le contenu du fichier n’est pas au format JSON (« ValueError »).

Le code suivant détaille bien les différents cas :

import json

try:
fichier = open("fichier.json", "r")
contenu = json.load(fichier)
print(contenu)
fichier.close()
except FileNotFoundError:
print("Le fichier demandé n'existe pas")
except PermissionError:
print("Le fichier existe mais pas de droit de lecture")
except ValueError:
print("Le fichier n'est pas au format json"

4.2 – Saisie d’un nombre entier

Lorsqu’on demande à l’utilisateur de saisir un entier, les erreurs possibles qui peuvent se produire sont :

  • L’utilisateur saisie « <CTRL>+<D> » pour la fin de fichier (L’exception levée dans ce cas est « EOFError »).
  • L’utilisateur saisie une chaîne de caractères qui ne peut être convertie en entier (l’exception dans ce cas est « ValueError »).
while True:
try:
n = int(input("Donner un nombre"))
break
except EOFError:
print("L'utilisateur n'a rien saisi")
except ValueError:
print("La saisie n'a pas pu être convertie en entier")

5 – Déclencher une exception

On utilise le mot clé « raise » pour lever une exception dans un code donné :

raise <objet exception>

Il arrive aussi que, dans un bloc « except », on veuille remonter la même exception pour qu’elle soit traitée à un niveau supérieur (une des fonctions appelantes). Pour cela il suffit juste de d’utiliser le mot clé « raise » sans préciser l’exception.

try:
<code susceptible de générer des exceptions>
except:
<traitement partiel de l’exception>
raise
# l'exception est remontée au niveau supérieur pour qu'elle soit
# traitée.

6 – Conclusion

Dans cet article, on a vu le concept d’exception en python qui permet la gestion des erreurs. Python définie une multitude d’exception, chacune prévue pour une type d’erreur particulier.

Il est possible de capturer une exception avec le les bloc « try … except … finally … », dans le but de la traiter et éviter l’arrêt brusque de l’exécution du programme.

On a vu qu’il est possible de créer nos propres types d’exceptions adaptées à l’application à développer.

Les structures de contrôle en Python

1 – Introduction

Les instructions ne s’exécutent pas toujours séquentiellement. En effet, il arrive qu’on veuille choisir entre deux ou plusieurs traitements à exécuter selon la situation au moment de l’exécution, les autres traitements seront alors ignorés. 

Il arrive également qu’un traitement doive se faire plusieurs fois mais on ne veut pas dupliquer cette partie du code car cela rendrait le programme très volumineux.

Dans cet article on va détailler les différentes structures de contrôle du flux d’exécution d’un programme.

2 – Les structures de contrôle

Il existe principalement deux types de structures de contrôle dans un programme : la structure alternative et les boucles (traitement répétitif).

2.1 – Structure alternative : « if » … « elif » … « else »

La structure alternative permet de faire en sorte qu’un traitement ne s’exécute que si une condition est vérifiée. Autrement dit, après évaluation de la condition, si cette dernière a pour valeur « True », le traitement sera exécuté, dans le cas contraire, le traitement est ignoré et on passe à la suite.

La syntaxe d’une structure alternative est :

if <condition 1> :
<traitement 1>
[
elif <condition 2> :
<traitement 2>
elif <condition 3> :
<traitement 3>
...
elif <condition n> :
<traitement n>
]
[
else :
<traitement else>
]

Voici quelques remarques concernant le code précédent :

  • La ligne qui précède un bloc de traitement se termine par deux points « : ».
  • Toutes les instructions d’un bloc de traitement sont décalées par rapport à la ligne qui introduit le bloc : l’indentation en python est très importante car c’est elle qui détermine le début et la fin d’un bloc.
  • Ce qui est entre crochets « [] » est facultatif. En effet, un « if » ne possède pas forcément des « elif » et un « else ».
  • Pour ce qui est des blocs « elif », on peut en mettre autant qu’on veut :  Il n’y a pas de limite.
  • Il n’y a qu’un seul bloc « else » au plus, et s’il est présent, il est mis à la fin.
  • Il n’y a qu’un seul traitement <traitement i> au plus qui sera exécuté, et cela dépend des conditions <condition i>. En effet, la première condition <condition i> à être évaluée (en commençant su haut vers le bas) à « True », le bloc correspondant <traitement i> sera exécuté.
  • S’il n’y a aucune condition qui est vraie, le bloc du « else » (s’il existe) sera exécuté.

Voici un exemple plus concret :

n = input("Entrer un nombre : ")
if n % 2 == 0:
print("pair")
else:
print("impair")

Le code précédent demande à l’utilisateur de saisir un nombre puis vérifie sa parité. S’il y est pair, il affiche « pair », sinon il affiche « impair ».

2.2 – Boucle « for »

Une boucle permet de définir un traitement itératif, c-à-d que c’est un bloc qui s’exécute 0 (zéro) ou plusieurs fois. On utilise le mot clé « for » pour définir une boucle dont le nombre d’itérations est connu à l’avance.

En python, pour écrire une boucle « for », on a besoin d’une variable itérable (C’est-à-dire une variable contenant plusieurs éléments et qu’on peut itérer). L’indice de la boucle va prendre les différentes valeurs des éléments contenus dans l’itérable au fil des itérations.

La syntaxe d’une boucle « for » est la suivante :

for x in <iterable>:
<traitement for>

Le bloc <traitement for> sera exécuté autant de fois que d’éléments dans <itérable>. À chaque itération, la variable « x » prend la valeur d’un élément de <itérable>.

Voici un exemple plus concret d’une boucle « for » :

for i in range(10):
print(i)

C’est une boucle « for » qui fait 10 itérations. la variable « i » prend les valeurs de 0 à 9.

2.3 – Boucle « while »

Le deuxième type de boucles c’est la boucle « while ». Comme la boucle « for », la boucle « while » permet de définir un traitement répétitif qui s’exécute 0 à plusieurs fois. Par contre, le nombre d’itération n’est pas connu à l’avance mais plutôt dépend d’une condition. En effet, la boucle  « while » définie une condition qui sera évaluer avant chaque début d’itération : Si la condition est vraie, le bloc s’exécute une fois de plus, sinon, la boucle se termine et l’exécution du programme continue à partir de l’instruction qui suit la boucle.

Quand il s’agit de la boucle « while », on parle souvent de condition d’arrêt. Cette dernière représente la condition pour que le programme sort de la boucle et continue en séquence. La condition d’arrêt est tout simplement la négation de la condition de boucle.

Voici la syntaxe d’une boucle « while » :

while <condition>:
<traitement while>

Voici un code python qui affiche les nombre de 0 à 9 avec une boucle « while » :

a = 0
while
a < 10:
    print(a)

3 – Conclusion

Dans cet article, nous avons vu les trois manières possibles de rompre la séquence en Python. on utilise la structure alternative « if … elif … else » pour effectuer un traitement particulier selon la valeur d’une ou plusieurs conditions.

On utilise les boucles « for » et « while » pour les traitements répétitifs (des traitements qui s’exécutent 0 ou plusieurs fois). Si on connait à l’avance le nombre d’itérations c’est la boucle « for » qui faut utiliser, dans le cas contraire, la boucle « while » est à utiliser.

Python : affectation vs. copy vs. deepcopy

1 – Introduction

Quand on écrit un programme, on a souvent besoin de sauvegarder le contenu d’une variable dans une autre variable avant de la modifier. Ou alors, on construit une variable à partir du contenu d’une autre variable et changer uniquement les parties qui diffèrent. Là encore, on a besoin de copier une variable dans une autre.

En python, les choses ne sont pas aussi simples. En effet, il y a trois niveaux de copies qu’on va détailler dans cet article.

2 – Différents niveaux de copie

Avant d’expliquer en détail, il serait intéressant de présenter certaines fonctions Python qui nous seront utiles.

  • La fonction prédéfinie « id » :

En regardant l’aide donné par le shell python, et ce, en faisant « help(id) », après traduction on aura ça :

id(obj, /)
Renvoie l’identité d’un objet.


Ceci est garanti d’être unique parmi tous les objets existants simultanéments.
(CPython utilise l’adresse mémoire de l’objet.)

C’est important de comprendre ce que renvoie la fonction « id » car on va l’utiliser juste après pour mieux expliquer les différentes manières de copier des variables.

2.1 – Affectation

Pour comprendre de quoi il s’agit, voici un exemple de code python : 

>>> l1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> l2 = l1

La deuxième ligne de ce code crée un nouvelle référence au même objet liste contenant les éléments de 1 jusqu’à 10. Il est important de souligner qu’il n’y a qu’un seul objet liste référencé par deux références : « l1 » et « l2 ».

On peut le vérifier en affichant l’identité des objets référencés « l1 » et « l2 » en faisant :

>> print(id(l1), id(l2), sep="\n")
45511432
45511432

Le résultat varie d’une exécution à une autre mais ce qui est sûr, c’est que la valeur est la même pour les deux références.

Une autre manière de vérifier qu’il n’y a qu’un seul objet référencé par les deux références, c’est en modifiant le contenu de « l1 », par exemple, puis voir le contenu de « l2 » :

>>> l1[0] = 100
>>> print(l1, l2, sep="\n")
[100, 2, 3, 4, 5, 6, 7, 8, 9, 10]
[100, 2, 3, 4, 5, 6, 7, 8, 9, 10]

On voit bien que c’est le même résultat pour les deux références, car, tout simplement, c’est le même objet qui a été modifié.

La figure suivante illustre bien ce qui se passe qu’on fait une affectation d’un objet à une variable (référence).

Figure 1 : Illustration d’un affectation

2.2 – copie simple (copy.copy)

Comme indiquer dans le titre, la fonction « copy » est définie dans le module python « copy ». Donc, pour pouvoir l’utiliser il faudra préalablement importer ce module en faisant :

import copy

Reprenons maintenant l’exemple précédent, et voyons ce qui se passe si on utilise la fonction « copy » au lieu d’une affectation :

>>> l1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> l2 = copy.copy(l1)
>>> print(id(l1), id(l2), sep="\n")
45511432
45833384

Remarquez ici, que les identités (donc les adresses) des objets référencés par « l1 » et « l2 » sont différentes, donc ça montre bien que ce sont deux objets distincts.

Prenant un autre exemple :

>>> l1 = [[1, 2, 3], [4, 5, 6]]
>>> l2 = copy.copy(l1)
>>> print(id(l1[0]), id(l2[0]), sep="\n")
45833640
45833640
>>> print(id(l1[1]), id(l2[1]), sep="\n")
45833544
45833544

Ceci montre que le premier élément (respectivement le deuxième élément) de chacune des deux listes « l1 » et « l2 » fait référence au même objet liste « [1, 2, 3] » (respectivement « [4, 5, 6] »). Si jamais on met à jour la valeur de « l1[0][0] », « l2[0][0] » sera modifié également.

La figure suivante illustre bien ce qui passe qu’on crée une copie en utilisant la fonction « copy.copy ».

Figure 2 : Illustration d’un copie simple avec « copy.copy »

La situation qu’on a vu précédemment peut être parfois gênante quand on a plusieurs niveaux de listes (c-à-d liste de listes de listes de …. ) ou d’objets de manière générale. En effet, il n’y a que le premier niveau (première liste) qui est dupliquer, mais les autres niveaux ne le sont pas.

Pour pallier à ce petit problème, il existe une manière pour faire une copie profonde qu’on va détailler juste après.

2.3 – copie profonde (dopy.deepcopy)

On a vu précédemment, « copy.deepcopy » permet de pallier à la limite de la copie simple en dupliquant tous les niveaux de listes (ou d’objets de manière générale). Ceci est fait de manière récursive dans le but de créer un nouvel objet identique au premier (Les deux ont les mêmes valeurs) mais qui sont totalement distincts.

Reprenons l’exemple précédent :

>>> l1 = [[1, 2, 3], [4, 5, 6]]
>>> l2 = copy.copy(l1)
>>> print(id(l1[0]), id(l2[0]), sep="\n")
45833448
33401128
>>> print(id(l1[1]), id(l2[1]), sep="\n")
45833384
45631656

Remarquez bien que les éléments des deux listes référencent des objets « liste » distincts. Car les valeurs de leurs identités sont différentes.

La figure suivante illustre bien ce qui se passe quand on fait une copie profonde avec « copy.deepcopy ».

Figure 3 : Illustration d’une copie profonde (deepcopy)

3 – Conclusion

Le but de cet est de bien montrer et expliquer la différence entre les trois niveaux de copies (affectation, copie simple, copie profonde). C’est très important de maîtriser ces notions pour éviter d’avoir des surprises à l’exécution d’un programme.

Commencer avec Tkinter

1 – Introduction

Tkinter est un module du langage Python, il est utilisé pour créer des interfaces graphiques. Il existe d’autres manières de faire des interfaces graphiques en Python (Utilisation de Qt par exemple), Cependant, Tkinter reste la bibliothèque la plus connue et la plus utilisée.

Le module « tkinter » est installé avec python, donc pas besoin de l’installer.

2 – Création d’une fenêtre basique

Une fenêtre graphique en « Tkinter » est une objet de type classe « Tk ». Cette classe est définie dans le module python qui s’appelle « tkinter ».

Il existe principalement trois manières pour créer une fenêtre graphique avec tkinter :

2.1 – Approche simple

J’ai appelé la première approche « approche simple » car elle nécessite pas des connaissance en programmation orientée objet python.

Avec cette approche tous les composants graphiques y compris notre fenêtre seront des variables « éparpillées » dans le programme.
Voici le code python pour la création d’une fenêtre basique :

#!/usr/bin/env python3
import tkinter as tk
Fenetre = tk.Tk()
# ... Ajouter des composants graphiques
Fenetre.mainloop()

Le code précédent affiche une fenêtre graphique vide : Aucun composant graphique dessus. Pour y ajouter des widgets, il faut les mettre dans la partie commentaire (avant dernière ligne).

Figure 1: Première fenêtre avec tkinter

2.2 – Approche par composition

La deuxième approche consiste à définir une classe, « Fenetre » par exemple, composée d’un objet de type classe « Tk ». La classe « Fenetre » possédera des attributs qui représenteront tous les composants graphiques de la fenêtre.

Le diagramme de classes UML qui correspond à cette approche est donné dans le figure suivante :

Figures 2 : Diagramme UML de composition

Voici un exemple de code qui permet de créer une fenêtre vide en utilisant l’approche par composition :

import tkinter as tk
class Fenetre:
def __init__(self):
f = tk.Tk()
# ... Ajouter des composants graphiques comme des attributs

f = Fenetre()
f.f.mainloop()

Le code précédent affiche une fenêtre vide comme celle de la figure 1.

2.3 – Approche par héritage

La troisième approche consiste à définir une classe, « Fenetre » par exemple, qui hérite de la classe « Tk ». Ainsi, la classe « Fenetre » va contenir tous ses composants graphiques comme des attributs.

La figure suivante montre le diagramme UML qui correspond à l’approche par héritage.

Figure 3 : Diagramme UML d’héritage

L’exemple ci-dessous permet de créer une fenêtre vide en utilisant l’approche par héritage :

import tkinter as tk
class Fenetre(tk.Tk):
  def __init__(self):
  tk.Tk.__init__(self)
# ... Ajouter des composants graphiques comme des attributs

f = Fenetre()
f.mainloop()

Le résultat est le même que celui montré dans figure 1.

3 – Conclusion

Parmi les trois approches présentées plus haut, il est toujours préféré d’opter pour la troisième approche surtout quand il s’agit de projets de grandes tailles. En effet, le code est mieux organisé et donc facile maintenir et à évoluer.

La première approche est adapté pour des programmes simples et/ou de petites tailles.

Premiers pas en Python

1 – Introduction

Si vous n’avez encore installer python, je vous conseille d’installer la dernière version qui, à ce jour, est python 3.8.1. Pour cela, vous pouvez suivre les étapes décrites dans un précédent article que vous pouvez trouver ici.

Vous savez sûrement que python est un langage interprété et non compilé. C’est-à-dire l’interpréteur python ne génère pas d’exécutable, il analyse le code ligne par ligne puis les exécute.

Autre particularité de Python, c’est le typage dynamique. En effet, pour utiliser une variable, on n’a pas besoin de déclarer son type. Au moment de la première affectation, python détecte le type de la valeur affectée. Puis, cette même variable peut changer de type si on lui affecte une valeur d’un autre type.

2 – Premier programme : « hello world »

Pour écrire un premier programme python sous linux, il faut créer un ficher texte qui s’appelle « hello-world.py » par exemple.

nano hello-world.py

Ensuite, il faut écrire le code suivant :

#!/usr/bin/env python3
print('Hello world')

<ctrl>+O pour sauvegarder. <ctrl>+X pour quitter  

Pour exécuter le programme, il y a deux manière. La première technique consiste à rendre le fichier exécutable et le lancer directement.

chmod +x hello-world.py
./hello-world

La deuxième manière, consiste à donner le fichier comme argument au programme python3.8.

python3.8 hello-world.py

3 – Interagir avec le shell python

Le site officiel met à votre disposition un shell python interactif pour découvrir le langage de programmation python. Mais pour ceux d’entre vous qui l’avez installé sur vos machines, Vous pouvez lancer le shell python en ouvrant un terminal linux, puis taper la commande :

python3.8

Si vous l’avez bien installé, normalement vous aurez quelque chose qui ressemble à ça :

Python 3.8.1 (default, Dec 21 2019, 19:58:48)
[GCC 8.3.0] on linux
Type « help », « copyright », « credits » or « license » for more information.
>>>

Les trois chevrons « >>> » indique que le shell python attend que vous entriez une instruction qu’il va exécuter pour vous.
Vous pouvez par exemple taper :

>>> print('hello-world')

La fonction « print » affiche ce qu’on lui donne en argument. C’est-à-dire, ici, elle affiche « hello-world ».

Autre instruction que vous pouvez essayer :

>>> a = 1
>>> b = 'Bonjour'
>>> pi = 3.14
>>> 1 + 1
2
>>> 9 * 9
81
>>>

Dans l’exemple précédent, on a défini des variables « a », « b », et « pi » qui valent respectivement « 1 », « ‘Bonjour' », et « 3.14 ».
On a utilisé également le shell python comme une calculatrice pour calculer la somme « 1+1 » et le produit « 9*9 ».

Pour quitter le shell, vous avez juste à écrire l’une de ces deux instructions :

>>> quit()

ou

>>> exit()

Et là vous revenez au terminal linux.