Résumé: Compléments de programmation en python: structures de contrôle, programmation fonctionnelle (map, reduce, lambda), introduction aux classes et objets.
L'objectif de ce tutoriel est d'introduire quelques outils et concepts plus avancés de la programmation en Python pour dans le but d'améliorer la performance et la lisibilité des codes. Les notions de classe, de programmation objet et celle de programmation fonctionnelle qui en découle sont fondamentales. Elles sont fondamentales pour le bon usage de certaines librairie dont Scikit-learn (section 3.3). C'est aussi une introduction à l'utilisation des fonctionnalités MapReduce parallélisables et donc à la base de l'algorithmique pour données distribuées (Hadoop) avec PySpark.
for
Une boucle for
permet, comme dans la pluspart des langages de programmation, de parcourir les éléments d'un objet itérable. En python cela peut-être une liste, un tuple, une chaîne de caractères (string), mais également des objets spécialement conçus pour cela, appelés iterator.
Syntaxe de la structure for
:
for
variable in range
iterator:
instruction
La ligne for
se termine par deux points ':'. Le bloc de codes à l'intérieur de la boucle est indenté.
Voici comment parcourir différentes structures itératives en se souvenant que: de façon générale, il faut éviter les boucles for
dans un langage interprété en utilisant les autres fonctionnalités (section 2) prévues dans les librairies pour parcourir des tableaux ou matrices.
range
La fonction range
produit une liste d'entiers mais aussi un objet itérable d'entiers utilisés au fur et à mesure des itérations de la boucle.
Attention xrange
de python2 est remplacé par range
en python3.
In [ ]:
for i in range(5):
print (i)
L'appel à la fonction range(n) permet d'itérer sur les entiers de 0 à n-1 mais il est possible de spécifier des intervalles des valeurs de chaque pas d'itération: range(2,11,2) range(10,0,-1)
In [ ]:
for character in "Hi There!":
print (character)
Un dictionnaire peut être parcouru terme à terme, cependant, comme la pluspart des objets python, les dictionnaires possèdent des fonctions permettant de les transformer en une liste:
.items()
.keys()
.values()
ou un iterator
.iteritems()
.iterkeys()
.itervalues()
Les dictionnaires n'ont pas de structure ordonnée, les valeurs ne s'affichent pas forcément dans l'ordre dans lequel on les a entrées.
In [ ]:
dico={"a":1,"b":2,"c":3}
for k in dico.keys():
print (k)
Les fonctions .items()
et .iteritems()
permettent de parcourir les couples (clés, objets) des dictionnaires.
In [ ]:
# Si une seule variable itérative est spécifiée, celle-ci est un-tuple.
for kv in dico.items():
print (kv)
In [ ]:
# Si deux variables itératives sont spécifiées, la première est la clé, la seconde la valeur correspondante
for k,v in dico.items():
print (k)
print (v)
In [ ]:
# Si la clé ou la valeur du dictionnaire n'est pas nécessaire, elle n'est pas stockée dans une variable en utilisant "_"
for k,_ in dico.items():
print (k)
In [ ]:
import numpy as np
a = np.arange(6).reshape(2,3)
for x in np.nditer(a):
print (x)
Appliquer directement la boucle for
sur le tableau Numpy permet de parcourir les différentes lignes de ce tableau.
In [ ]:
for x in a:
print (x)
Pour parcourir les colonnes, la métode la plus simple consiste à parcourir les lignes de la matrice transposée.
In [ ]:
for x in a.T:
print (x)
Il est cependant très important de comprendre que les arrays numpy ont été conçus pour appliquer des fonctions directement sur l'ensemble du tableau en évitant des boucles. Ainsi nombre de fonctions natives permettent de résoudre de nombreux problèmes sans avoir à parcourir l'ensemble de la matrice: sum
, max
, etc... permettent d'obtenir les résultats escomptés de manière bien plus rapide qu'en utilisant les boucles for
.
In [ ]:
mina = a.max() # Retourne la valeur maximum de l'array a
minxa = a.max(axis=1) # Retourne la valeur maximum de chaque ligne de l'array a
minya = a.max(axis=0) # Retourne la valeur maximum de chaque colonne de l'array a
print(mina, minxa, minya)
Pour appliquer des fonctions plus complexes sur chaque ligne et/ou colonne de l'array, il existe également des fonctions natives de la librairie numpy permettant d'appliquer ces fonctions efficacement comme par exemple apply_along_axis
, similaire à apply
de R.
In [ ]:
def scale(x):
xcr = (x-np.mean(x))/np.std(x)
return xcr
X = np.random.randint(5, size=(3, 3))
Xcr = np.apply_along_axis(scale,1,X)
print(X, Xcr)
De la même manière que pour les arrays numpy, les DataFrame Pandas ne sont pas conçus pour être parcourus facilement. Il est, là encore, préférable d'utiliser au maximum les méthodes natives de pandas ou de numpy, ou de convertir au préalable les colonnes souhaitées en listes ou encore même de convertir un DataFrame en array avant un traitemnte itératif.
Il éxiste deux fonctions permettant de parcourir les différentes colonnes d'une DataFrame Pandas:
iterrows()
: retourne le couple (index, ligne). Chaque ligne est parcourue sous la forme d'un objet Series de Pandas. Se construction est très coûteuse et rend la méthode très lente .itertuples()
: retoure un object specifique Pandas, où l'index, et les valeurs des colonnes de chaque ligne est accesible via des méthodes spécifiques. Plus rapide que iterrows
.
In [ ]:
import pandas as pd
df = pd.DataFrame([["A",4],["B",5],["C",6]],index=[1,2,3],columns=["Letter","Number"])
for i,r in df.iterrows():
print(i,r)
for ir in df.itertuples():
print(ir)
La librairie native de python itertools possède de nombreuses fonctions générant des iterators. Plus d'exemples dans la documentation.
In [ ]:
import itertools
#zip : Concatenne les éléments de plusieurs objets itérables
zip_list = []
for k in zip('ABCD',[1,2,3,4],"wxyz"):
zip_list.append(k)
print("zip")
print(zip_list)
In [ ]:
#permutation : retourne tous les arrangements possibles de liste de longueur n.
permutation_list = []
for k in itertools.permutations("ABCD",2):
permutation_list.append(k)
print("permutations")
print(permutation_list)
In [ ]:
# Version1
A1=[]
for k in range(10):
A1.append(k*k)
# Version 2
A2 = [k*k for k in range(10)]
print(A1,A2)
In [ ]:
a = 17
s = "hi"
a += 3 # Equivalent to a = a + 3
a -= 3 # Equivalent to a = a - 3
a *= 3 # Equivalent to a = a * 3
a /= 3 # Equivalent to a = a / 3
a %= 3 # Equivalent to a = a % 3
s += " there" # Equivalent to s = s + “ there"
while
La boucle for permet de parcourir l'ensemble des éléments d'un objet itérable, ou un nombre déterminé d'éléments de ce dernier. Cependant, ce nombre n'est pas toujours prévisible et il est possible de parcourir ces éléments et arrêter le parcours lorsqu'une condition est respéctée ou non. C'est l'objet de l'instruction while
avec la syntaxe:
while
condition:
instructions
Cette instruction permet de répéter en boucle les instructions jusqu'à ce que la condition soit vérifiée.
In [ ]:
# Incrémentation de `count` jusqu'à ce qu'elle dépasse la valeur 100.000
count = 1
while count <= 100000:
count += 1
print (count)
L'instruction break
permet de sortir de la boucle while
même si sa condition est respectée.
In [ ]:
while True:
number = int(input("Enter the numeric grade: "))
if number >= 0 and number <= 100:
break
else:
print ("Error: grade must be between 100 and 0" )
print (number)
In [ ]:
number=1
if number==1:
print (True)
else:
print (False)
Lorsque plus de deux alternative sont possibles, utiliser l'instruction elif
pour énumérer les différentes possibilités.
In [ ]:
number=13
if number<5:
print("A")
elif number <10:
print("B")
elif number <20:
print("C")
else:
print("D")
In [ ]:
number=10
"A" if number >10 else "B"
In [ ]:
#Sélectionne uniquement les valeur paire
l1 = [k for k in range(10) if k%2==0]
l1
In [ ]:
#Retourne "even" si l'élement k est pair, "odd" sinon.
l2 = ["even" if k%2==0 else "odd" for k in range(10)]
l2
L'utilisation de higher-order functions permet d'éxecuter rapidement des schémas classiques:
Important: il s'agit ici d'introduitre les éléments de programmation fonctionnelle, présents dans Python, et utilisés systématiquement, car parallélisable, dans des architectures distribuées (e. g. Hadoop, Spark).
map
La première de cette fonction est la fonction map
. Elle permet d'appliquer une fonction sur toutes les valeurs d'une liste et retourne une nouvelle liste avec les résultats correspondants.
In [ ]:
import random
numbers = [random.randrange(-10,10) for k in range(10)]
abs_numbers = map(abs,numbers) # Applique la fonction "valeur absolue" à tout les élements de la liste
print(numbers,list(abs_numbers))
In [ ]:
def first_capital_letters(txt):
if txt[0].islower():
txt = txt[0].upper()+txt[1:]
return txt
name=["Jason","bryan","hercule","Karim"]
list(map(first_capital_letters,name))
filter
La fonction filter
permet d'appliquer une fonction test à chaque valeur d'une liste. Si la fonction test est vérifiée, la valeur est ajoutée dans une nouvelle liste. Sinon, la valeur n'est pas prise en compte. La nouvelle liste, constitué de toutes les valeures "positive" selon la fonction test, est retournée.
In [ ]:
def is_odd(n):
return n % 2 == 1
list(filter(is_odd,range(20)))
reduce
La dernière fonction est la fonction reduce
. Cette approche est loin d'être intuitive. Le meilleur moyen de comprendre le mode d'emploi de cette fonction est d'utiliser un exemple.
L'objectif est de calculer la somme de tous les entier de 0 à 9.
r10 = [0,1,2,3,4,5,6,7,8,9]
reduce
, applique une première fois la fonction sum_and_print
sur les deux premiers éléments de la liste. sum_and_print
est appliquée sur le résultat de la première opération et sur le 3ème éléments de la listeLa fonction reduce
a donc deux arguments: une fonction et une liste.
In [ ]:
import functools
def sum_and_print(x,y):
print("Input: ", x,y)
print("Output: ", y)
return x+y
r10 = range(10)
res =functools.reduce(sum_and_print, r10)
print(res)
Par défaut, la fonction passée en paramètre de la fonction reduce
effectue sa première opérations sur les deux premiers éléments de la listes passés en paramètre. Mais il est possible de spécifier une valeur initiale en troisième paramètre. La première opération sera alors effectuée sur cette valeur initiale et le premier élément de la liste.
In [ ]:
def somme(x,y):
return x+y
r10 = range(10)
res =functools.reduce(somme, r10,1000)
print(res)
L'utilisation de fonctions génériques permet de simplifier le code mais il est coûteux de définir une nouvelle fonction qui peut ne pas être réutilisée, comme par exemple celle de l'exemple précédent.
L'appel lambda
permet de créer une fonction de façon temporaire. La définition de ces fonctions est assez restrictive, puisqu'elle implique une définition sur une seule ligne, et ne permet pas d'assignation.
Autre point important, l'exécution de cette fonction sur des données distribuées est implicitement parallélisée.
Ainsi les précédents exemples peuvent-être ré-écrits de la manière suivante:
In [ ]:
name=["Jason","bryan","hercule","Karim"]
list(map(lambda x : x[0].upper()+x[1:] if x[0].islower() else x,name))
In [ ]:
list(filter(lambda x : x % 2 == 1 ,range(10)))
In [ ]:
r10 = range(10)
res =functools.reduce(lambda x,y:x+y, r10,1000)
res
Les classes sont des objets communs à tous les langages orientés objets. Ce sont des objets constitués de
Ci dessous on définit une classe "Elève" dans laquelle un élève est décrit par son nom, son prénom et ses notes. On notera la convention de nommage des méthodes qui commence par une minuscule, mais qui possède une majuscule à chaque début de nouveau mot.
In [ ]:
class Eleve:
"""Classe définissant un élève caractérisé par:
- son nom
- son prénom
- ses notes
"""
def __init__(self, nom, prenom): #constructeur de la classe
""" Construit un élève avec les nom et prenom passé en paramètre et une liste de notes vide."""
self._nom = nom
self._prenom=prenom
self._notes = []
def getNom(self):
""" retourne le nom de l'élève """
return self._nom
def getNotes(self):
""" retourne les notes de l'élève"""
return self._notes
def getNoteMax(self):
""" retourne la note max de l'élève"""
return max(self._notes)
def getMean(self):
""" retourne la moyenne de l'élève"""
return np.mean(self._notes)
def getNbNote(self):
""" retourne le nombre de note de l'élève"""
return len(self._notes)
def addNote(self, note):
""" ajoute la note 'note' à la liste de note de l'élève"""
self._notes.append(note)
Toutes les classes sont composées d'un constructeur qui a pour nom __init__. Il s'agit d'une méthode spéciale d'instance que Python reconnaît et sait utiliser dans certains contextes. La fonction __init__ est automatiquement appelée à la création d'une nouvelle classe et prend en paramètre self
, qui représente l'objet instantié, et les différents attributs nécessaires à sa création.
In [ ]:
eleve1 = Eleve("Jean","Bon")
eleve1._nom
Les attributs de la classe sont directement accessibles de la manière suivante:
objet.nomDeLAtribbut
Cependant, par convention, il est conseillé de définir une méthode pour avoir accès à cet objet.
Les méthodes qui permettent d'accéder à des attributs de l'objets sont appelés des accessors. Dans la classe élève, les méthodes commençant par get
sont des accessors.
In [ ]:
eleve1.getNom()
Les méthodes permettant de modifier les attributs d'un objet sont appelées des mutators. La fonction addNote
, qui permet d'ajouter une note à la liste de notes de l'élève est un mutator.
In [ ]:
print(eleve1.getNotes())
eleve1.addNote(15)
print(eleve1.getNotes())
In [ ]:
for k in range(10):
eleve1.addNote(np.random.randint(20))
In [ ]:
print (eleve1.getNbNote())
print (eleve1.getNoteMax())
print (eleve1.getMean())
In [ ]:
class EleveSpecial(Eleve):
def __init__(self, nom, prenom, optionName):
Eleve.__init__(self, nom, prenom)
self._optionName = optionName
self._optionNotes = []
def getNotesOption(self):
""" retourne les notes de l'élève"""
return self._optionNotes
def addNoteOption(self, note):
""" ajoute la note 'note' à la liste de note de l'élève"""
self._optionNotes.append(note)
In [ ]:
eleve2 = EleveSpecial("Sam","Stress","latin")
In [ ]:
eleve2.addNote(14)
print (eleve2.getNotes())
eleve2.addNoteOption(12)
print (eleve2.getNotesOption())
Les méthodes d'apprentissage statistique de la librairie Scikit-learn sont un parfait exemple d'utilisation de classes. Sans entrer en détail dans l'implémentation de cette méthode, voici l'exemple de la Régression linéaire par moindres carrés.
Toutes les fonctions de Scikit-learn sont définies sur de modèle. Voir dans le tutoriel d'apprentissage statistique avec Scikit-learn comment ces propriétés sont utilisées pour enchaîner (pipeline) des exécutions.
L'objet LinearRegression
est une classe qui est initée avec des attributs suivants:
fit_intercept
: si le terme constant doit être estimé ou considéré à 0normalize
: si le jeux d'apprentissage doit être normalisé avant la regressioncopy_X
: si le jeux d'apprentissage doit être copié pour éviter les effets de bordsn_jobs
: le nombre de processeur à utiliser
In [ ]:
from sklearn.linear_model import LinearRegression
lr = LinearRegression(fit_intercept=True, normalize=False, copy_X=True, n_jobs=1)
print (lr.fit_intercept, lr.normalize, lr.copy_X, lr.n_jobs)
La classe LinearRegression
possède également des attributs qui sont mis à jour à l'aide des méthodes:
coef_
: coefficients estimés residues_
: somme des résidusintercept_
: terme constantLa méthode fit
de cette classe est un mutator. Elle prend en paramètre le jeux d'apprentissage, X_train
et la variable réponse correspondante Y_train
pour estimer les paramètres de la regression linéaire et ainsi mettre à jour les attributs correspondants.
In [ ]:
X_train=[[0, 0], [1, 1], [2, 2]]
Y_train = [0, 1, 2]
lr.fit (X_train, Y_train)
lr.coef_
La classe LinearRegression
possède aussi d'autres méthodes qui utilisent les attributs de la classe. Par exemple:
predict
: estime la prévision de la variable réponse d'un jeu test X_test
score
: retourne le coefficient R2 de qualité de la prévision.
In [ ]:
X_test = [[1.5,1.5],[2,4],[7.3,7.1]]
Y_test = [1.5,2.4,7]
pred = lr.predict(X_test)
s = lr.score(X_test,Y_test)
print(pred,s)
In [ ]:
def unpacking_list_and_print(a, b):
print (a)
print (b)
listarg = [3,4]
unpacking_list_and_print(*listarg)
def unpacking_dict_and_print(k1=0, k2=0):
print (k1)
print (k2)
dictarg = {'k1':4, 'k2':8}
unpacking_dict_and_print(**dictarg)
Ces opérateurs sont surtout utiles dans le sens du "packing". Les fonctions sont alors définies de sorte à recevoir un nombre inconnu d'argument qui seront ensuite "paquétés" et traités dans la fonction.
L'argument *args
permet à la fonction de recevoir un nombre supplémentaire inconnu d'arguments sans mot-clef associé.
In [ ]:
def packing_and_print_args(required_arg, *args):
print ("arg Nécessaire:", required_arg)
for i, arg in enumerate(args):
print ("args %d:" %i, arg)
packing_and_print_args(1, "two", 3)
packing_and_print_args(1, "two", [1,2,3],{"a":1,"b":2,"c":3})
L'argument **kwargs
permet à la fonction de recevoir un nombre supplémentaire inconnu d'arguments avec mot-clef.
In [ ]:
def packing_and_print_kwargs(def_kwarg=2, **kwargs):
print ("kwarg défini:", def_kwarg)
for i,(k,v) in enumerate(kwargs.items()):
print ("kwarg %d:" %i ,k , v)
packing_and_print_kwargs(def_kwarg=1, sup_arg1="two", sup_arg2=3)
packing_and_print_kwargs(sup_arg1="two", sup_arg2=3, sup_arg3=[1,2,3])
Les arguments *args
et **kwargs
peuvent être combinés dans une autre fonctions.
In [ ]:
def packing_and_print_args_and_kwargs(required_arg ,def_kwarg=2, *args, **kwargs):
print ("arg Nécessaire:", required_arg)
for i, arg in enumerate(args):
print ("args %d:" %i, arg)
print ("kwarg défini:", def_kwarg)
for i,(k,v) in enumerate(kwargs.items()):
print ("kwarg %d:" %i ,k , v )
packing_and_print_args_and_kwargs(1, "two", [1,2,3] ,sup_arg1="two", sup_arg2=3 )
Ces deux opérateurs sont très utiles pour gérer des classes liées par des héritages. Les arguments *args **kwargs
permettent alors de gérer la tranmission de cet héritage sans avoir à redéfinir les arguments à chaque étape.
In [ ]:
class Objet(object):
def __init__(self, attribut=None, *args, **kwargs):
print (attribut)
class Objet2Point0(Objet):
def __init__(self, *args, **kwargs):
super(Objet, self).__init__(*args, **kwargs)
class Objet3Point0(Objet2Point0):
def __init__(self,attribut2=None, *args, **kwargs):
super(Objet2Point0, self).__init__(*args, **kwargs)
print (attribut2)
my_data = {'attribut': 'Argument1', 'attribut2': 'Argument2'}
Objet3Point0(**my_data)