Lecture Notes

Collected lectures notes in computational biology

Antisèches Python pour Biologiste Pressé

Voici un looooong notebook qui peut vous servir de référence sur Python. Ce n'est en aucun cas un substitut à un cours complet, mais la vraie maîtrise des outils de programmation ne s’acquiert que par la pratique régulière. N'hésitez pas à garder ceci sous le coude quand vous essayez de résoudre les problèmes que vous rencontrez.

Les Parties 1-6 sont dérivées de https://learnxinyminutes.com/ (CC BY-SA 3.0)

Jupyter Notebook

Jupyter est un logiciel écrit en python permettant de travailler avec des notebook dans différents languages (dont Julia, Python et R d'où le nom du logiciel). Vous pouvez le lancer sur les machines de la salle info en tapant jupyter notebook dans un terminal

Pointez ensuite votre navigateur favori sur l'adresse indiquée (en général https://localhost:8888 )

Le Notebook est organisé en cellules qui peuvent contenir soit du code soit du texte formaté en Markdown et des équations formatées en $\LaTeX$.

Si vous avez chargé le notebook (fichier .ipynb) dans jupyter, n'hésitez pas à editer ce texte en double cliquant dessus.

Les notebook peuvent aussi être exportés dans divers formats dont des pages web (.html) en utilisant le menu idoine (File->Download as)

Raccourcis clavier utiles

  • Ctrl-M a et Ctrl-M b: Créer une nouvelle cellule au-dessus/au-dessous de la cellule sélectionnée.
  • Ctrl-M c, Ctrl-M v, Ctrl-M x : Copier, coller et couper (resp.) des cellules.
  • Shift+Entrée : Exécuter le contenu de la cellule courante
  • Ctrl-M m, Ctrl-M y: Passer la cellule en mode texte ou en mode code.
  • Shift+Tab sur le nom d'une fonction : Afficher l'aide, C'est à dire sa docstring.

Types de données primaires et opérateurs

Entiers, flottants

In [14]:
# Le croisillon (#) indique que le reste de la ligne est un commentaire.

# On a des nombres entiers
3  # => 3
type(3) #=> int

# Les calculs sont ce à quoi on s'attend (+, -, *)
1 + 1  # => 2
8 - 1  # => 7
10 * 2  # => 20

# On peut forcer la priorité de calcul avec des parenthèses
(1 + 3) * 2  # => 8

# La division retourne un nombre à virgule flottante (float)
35 / 5  # => 7.0
type(35/5) #=> float

# Quand on utilise un float, le résultat est un float
3 * 2.0 # => 6.0

# La Division Euclidienne se fait avec une double barre oblique. 
5 // 3     # => 1
type(5//3) #=> int
5.0 // 3.0 # => 1.0 # Ça marche pour les floats egalement. 
-5 // 3  # => -2
-5.0 // 3.0 # => -2.0

# Modulo (reste de la division)
7 % 3 # => 1

# Exponentiation (x**y, x élevé à la puissance y)
2**4; # => 16

Booléens

In [15]:
# Les valeurs booléennes sont de type "bool"
True
False
type(True) #=> bool

# Négation avec not
not True  # => False
not False  # => True

# Opérateurs booléens
# On note que "and" et "or" sont sensibles à la casse
True and False #=> False
False or True #=> True

# On vérifie une égalité avec ==, cela renvoie un bool.
1 == 1  # => True
2 == 1  # => False
type(1==1) #=> bool

# On vérifie une inégalité avec !=
1 != 1  # => False
2 != 1  # => True

# Autres opérateurs de comparaison
1 < 10  # => True
1 > 10  # => False
2 <= 2  # => True
2 >= 2  # => True

# On peut enchaîner les comparaisons
1 < 2 < 3  # => True
2 < 3 < 2  # => False

# Utilisation des opérations booléennes avec des entiers :
# Tout entier non null est "True"
0 == False #=> True
2 == True #=> False
1 == True #=> True
0 and 2 #=> 0
-5 or 0 #=> -5

# (is vs. ==) is vérifie si deux variables pointent sur le même objet, mais == vérifie
# si les objets ont la même valeur.
a = [1, 2, 3, 4] # a pointe sur une nouvelle liste, [1, 2, 3, 4]
b = a # b pointe sur a
b is a # => True, a et b pointent sur le même objet
b == a # => True, les objets a et b sont égaux
b = [1, 2, 3, 4] # b pointe sur une nouvelle liste, [1, 2, 3, 4]
b is a # => False, a et b ne pointent pas sur le même objet
b == a; # => True, les objets a et b ne pointent pas sur le même objet

Chaînes de caractères

In [17]:
# Les chaînes (ou strings) sont créées avec " ou '
"Ceci est une chaine"
'Ceci est une chaine aussi.'

""" Les chaînes de caractères peuvent aussi être écrites
    avec 3 guillemets doubles ("), et sont souvent
    utilisées comme des commentaires.
"""

# Additionner des chaînes les concatène. 
"Hello " + "world!"  # => "Hello world!"
# Deux chaînes juxtaposées sont aussi concaténées. 
"Hello " "world!"  # => "Hello world!"
("Combiné aux parenthèses, cela permet de représenter une "
"longue chaîne (qui ne contient pas de retour à la ligne)"
 "sur plusieurs ligne.")

# On peut traîter une chaîne comme une liste de caractères
"This is a string"[0]  # => 'T'

# .format peut être utilisé pour formatter des chaînes, comme ceci:
"{} peuvent etre {}".format("Les chaînes", "interpolées")

# La méthode format a un mini langage complet pour formatter les chaînes de caractères
# (https://docs.python.org/3/library/string.html#formatspec)
Out[17]:
'Les chaînes peuvent etre interpolées'
In [78]:
# On peut aussi réutiliser le même argument pour gagner du temps.
"{0} be nimble, {0} be quick, {0} jump over the {1}".format("Jack", "candle stick")
Out[78]:
'Jack be nimble, Jack be quick, Jack jump over the candle stick'
In [79]:
# On peut aussi utiliser des mots clés pour éviter de devoir compter.
"{name} wants to eat {food}".format(name="Bob", food="lasagna")
Out[79]:
'Bob wants to eat lasagna'
In [117]:
# Python a une fonction print pour afficher du texte
print("I'm Python. Nice to meet you!")

# Par défaut, la fonction print affiche aussi une nouvelle ligne à la fin.
# Utilisez l'argument optionnel end pour changer ce caractère de fin.
print("Hello, World", end="!") # => Hello, World!
I'm Python. Nice to meet you!
Hello, World!
In [25]:
liste =  [1,2,3]
str(liste)[1:-1]
Out[25]:
'1, 2, 3'
In [18]:
# La méthode Split permet de découper une chaîne par rapport à un séparateur
"human_genome_200.txt".split("_") #=> ['human', 'genome', '200.txt']
# La méthode join permet de recoller une liste de chaîne à partir d'un séparateur
print("\n".join(['human', 'genome', '200.txt'])) #=> 'human_genome_200.txt'
human
genome
200.txt

Un type spécial, None

In [ ]:
# None est un objet
None  # => None

# N'utilisez pas "==" pour comparer des objets à None
# Utilisez plutôt "is". Cela permet de vérifier l'égalité de l'identité des objets.
"etc" is None  # => False
None is None  # => True

Variables

In [19]:
# Les variables en Python sont des référence nommées à des objets. 
# La convention est de nommer ses variables avec des minuscules_et_underscores
some_var = 5 # Créer une variable qui pointe vers l'objet "5" de type int. 
some_var
Out[19]:
5
In [26]:
# Tenter d'accéder à une variable non définie lève une exception.
# Voir Structures de contrôle pour en apprendre plus sur le traitement des exceptions.
une_variable_inconnue  # Lève une NameError
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-26-b986950689af> in <module>()
      1 # Tenter d'accéder à une variable non définie lève une exception.
      2 # Voir Structures de contrôle pour en apprendre plus sur le traitement des exceptions.
----> 3 une_variable_inconnue  # Lève une NameError

NameError: name 'une_variable_inconnue' is not defined

Structures de données

Listes (ordonné, modifiable)

In [20]:
# Les listes permettent de stocker des séquences
li = []

# On peut initialiser une liste pré-remplie
other_li = [4, 5, 6]

# On ajoute des objets à la fin d'une liste avec .append
li.append(1)    # li vaut maintenant [1]
li.append(2)    # li vaut maintenant [1, 2]
li.append(4)    # li vaut maintenant [1, 2, 4]
li.append(3)    # li vaut maintenant [1, 2, 4, 3]

# On enlève le dernier élément avec .pop
li.pop()        # => 3 et li vaut maintenant [1, 2, 4]
# Et on le remet
li.append(3)    # li vaut de nouveau [1, 2, 4, 3]

# Accès à un élément d'une liste :
li[0]  # => 1
# Accès au dernier élément :
li[-1];  # => 3
In [21]:
# Accéder à un élément en dehors des limites lève une IndexError
li[4]  # Lève une IndexError
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-21-e8f9f5e370cd> in <module>()
      1 # Accéder à un élément en dehors des limites lève une IndexError
----> 2 li[4]  # Lève une IndexError

IndexError: list index out of range
In [24]:
# On peut accéder à un intervalle avec la syntaxe "slice"
li = [1,2,3,4]
li[1:3]  # => [2, 4]
# Omettre les deux premiers éléments
li[2:]  # => [4, 3]
# Prendre les trois premiers
li[:3]  # => [1, 2, 4]
# Sélectionner un élément sur deux
li[::2]   # =>[1, 4]
# Avoir une copie de la liste à l'envers
li[::-1]   # => [3, 4, 2, 1]

# La syntaxe est: debut:fin:pas
# Le premier element est inclusif (en partant de 0)
# Le dernier element est exclusif
# Le pas est optionnel

# Faire une copie d'une profondeur de un avec les "slices"
li2 = li[:] # => li2 = [1, 2, 4, 3] mais (li2 is li) vaut False.

# Enlever des éléments arbitrairement d'une liste
del li[2]   # li is now [1, 2, 3]

# On peut additionner des listes pour les concaterner
# Note: les valeurs de li et other_li ne sont pas modifiées.
li + other_li   # => [1, 2, 3, 4, 5, 6]

# Vérifier la présence d'un objet dans une liste avec "in"
1 in li   # => True

# Examiner la longueur avec "len()"
len(li);   # => 6
In [27]:
# Trier les listes se fait avec la méthode sort pour un tri "sur place"
a = [24, 324, 2, 8]
a.sort() #=> a is now [2, 8, 24, 324]

# ou la fonction sorted pour un qui retourne une nouvelle liste sans changer la première
a = [24, 324, 2, 8]
b = sorted(a) #=> [2, 8, 24, 324]
a,b
Out[27]:
([24, 324, 2, 8], [2, 8, 24, 324])
In [28]:
# Par défaut les listes sont triées dans l'ordre croissant.
# On peut changer la fonction de comparaison utilisée pour le tri avec l'argument key.

# Ex: tri dans l'ordre décroissant. 
a = [24, 324, 2, 8]
sorted(a) #=> [2, 8, 24, 324]
def ordre(x):
    return -x
sorted(a, key=ordre) #=> [324, 24, 8, 2]

# Ex: tri dans l'ordre du second elément d'une paire
a = [(1,2),(3,4),(5,0)]
sorted(a) #=> [(1, 2), (3, 4), (5, 0)]
def ordre(x):
    return x[1]
sorted(a, key=ordre) #=> [(5, 0), (1, 2), (3, 4)]
Out[28]:
[(5, 0), (1, 2), (3, 4)]

N-uplets ou Tuples (ordonné, immuable)

In [28]:
# Les tuples sont comme des listes mais sont immuables
tup = (1, 2, 3)
tup[0]   # => 1
tup[0] = 3  # Lève une TypeError
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-28-6f49ff0631d1> in <module>()
      2 tup = (1, 2, 3)
      3 tup[0]   # => 1
----> 4 tup[0] = 3  # Lève une TypeError

TypeError: 'tuple' object does not support item assignment
In [34]:
# Note : un tuple de taille un doit avoir une virgule après le dernier élément,
# mais ce n'est pas le cas des tuples d'autres tailles, même zéro.
type((1))  # => <class 'int'>
type((1,)) # => <class 'tuple'>
type(())   # => <class 'tuple'>

# On peut utiliser la plupart des opérations des listes sur des tuples.
len(tup)   # => 3
tup + (4, 5, 6)   # => (1, 2, 3, 4, 5, 6)
tup[:2]   # => (1, 2)
2 in tup   # => True

# Vous pouvez décomposer des tuples (ou des listes) dans des variables
a, b, c = (1, 2, 3)     # a vaut 1, b vaut 2 et c vaut 3
# Les tuples sont créés par défaut sans parenthèses
d, e, f = 4, 5, 6
# Voyez comme il est facile d'intervertir deux valeurs :
e, d = d, e     # d vaut maintenant 5 et e vaut maintenant 4

Dictionnaires (clé/valeur, non-ordonné, modifiable)

In [27]:
# Créer un dictionnaire :
empty_dict = {}
# Un dictionnaire pré-rempli :
filled_dict = {"one": 1, "two": 2, "three": 3}
In [28]:
# Note : les clés des dictionnaires doivent être de types immuables.
# Elles doivent être convertibles en une valeur constante pour une recherche rapide.
# Les types immuables incluent les ints, floats, strings et tuples.
invalid_dict = {[1,2,3]: "123"} # => Lève une TypeError: unhashable type: 'list'
valid_dict = {(1,2,3):[1,2,3]}  # Par contre, les valeurs peuvent être de tout type.
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-28-673be194e885> in <module>()
      2 # Elles doivent être convertibles en une valeur constante pour une recherche rapide.
      3 # Les types immuables incluent les ints, floats, strings et tuples.
----> 4 invalid_dict = {[1,2,3]: "123"} # => Lève une TypeError: unhashable type: 'list'
      5 valid_dict = {(1,2,3):[1,2,3]}  # Par contre, les valeurs peuvent être de tout type.

TypeError: unhashable type: 'list'
In [29]:
# On trouve une valeur avec []
filled_dict["one"]   # => 1

# On obtient toutes les clés sous forme d'un itérable avec "keys()" Il faut l'entourer
# de list() pour avoir une liste Note: l'ordre n'est pas garanti.
list(filled_dict.keys())   # => ["three", "two", "one"]

# On obtient toutes les valeurs sous forme d'un itérable avec "values()". 
# Là aussi, il faut utiliser list() pour avoir une liste.
# Note : l'ordre n'est toujours pas garanti.
list(filled_dict.values())   # => [3, 2, 1]

# On vérifie la présence d'une clé dans un dictionnaire avec "in"
"one" in filled_dict   # => True
1 in filled_dict;   # => False
In [30]:
# L'accès à une clé non-existente lève une KeyError
filled_dict["four"]   # KeyError
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-30-80440d700559> in <module>()
      1 # L'accès à une clé non-existente lève une KeyError
----> 2 filled_dict["four"]   # KeyError

KeyError: 'four'
In [ ]:
# On utilise "get()" pour éviter la KeyError
filled_dict.get("one")   # => 1
filled_dict.get("four")   # => None
# La méthode get accepte une valeur de retour par défaut en cas de valeur non-existante.
filled_dict.get("one", 4)   # => 1
filled_dict.get("four", 4)   # => 4

# "setdefault()" insère une valeur dans un dictionnaire si la clé n'est pas présente.
filled_dict.setdefault("five", 5)  # filled_dict["five"] devient 5
filled_dict.setdefault("five", 6)  # filled_dict["five"] est toujours 5

# Ajouter à un dictionnaire
filled_dict.update({"four":4}) #=> {"one": 1, "two": 2, "three": 3, "four": 4}
#filled_dict["four"] = 4  # une autre méthode

# Enlever des clés d'un dictionnaire avec del
del filled_dict["one"]  # Enlever la clé "one" de filled_dict.

Ensembles (non-ordonné, modifiable)

In [ ]:
# Les sets stockent des ensembles
empty_set = set()
# Initialiser un set avec des valeurs. 
some_set = {1, 1, 2, 2, 3, 4}   # some_set est maintenant {1, 2, 3, 4}
In [81]:
# Comme les clés d'un dictionnaire, les éléments d'un set doivent être immuables.
valid_set = {(1,), 1}
invalid_set = {[1], 1} # => Lève une TypeError: unhashable type: 'list'
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-81-f9738ce9c4f8> in <module>()
      1 # Comme les clés d'un dictionnaire, les éléments d'un set doivent être immuables.
      2 valid_set = {(1,), 1}
----> 3 invalid_set = {[1], 1} # => Lève une TypeError: unhashable type: 'list'

TypeError: unhashable type: 'list'
In [41]:
# On peut changer un set :
filled_set = some_set

# Ajouter un objet au set :
filled_set.add(5)   # filled_set vaut maintenant {1, 2, 3, 4, 5}

# Chercher les intersections de deux sets avec &
other_set = {3, 4, 5, 6}
filled_set & other_set   # => {3, 4, 5}

# On fait l'union de sets avec |
filled_set | other_set   # => {1, 2, 3, 4, 5, 6}

# On fait la différence de deux sets avec -
{1, 2, 3, 4} - {2, 3, 5}   # => {1, 4}

# On vérifie la présence d'un objet dans un set avec in
2 in filled_set   # => True
10 in filled_set   # => False
Out[41]:
False
In [18]:
# None, 0, et les strings/lists/dicts (chaînes/listes/dictionnaires) vides valent False
# lorsqu'ils sont convertis en booléens.
# Toutes les autres valeurs valent True
bool(0)  # => False
bool("")  # => False
bool([]) #=> False
bool({}) #=> False
Out[18]:
False

Structures de contrôle et Itérables

In [2]:
# On crée juste une variable
some_var = 5

# Voici une condition "si". L'indentation est significative en Python!
# Affiche: "some_var is smaller than 10"
if some_var > 10:
    print("some_var is totally bigger than 10.")
elif some_var < 10:    # La clause elif ("sinon si") est optionelle
    print("some_var is smaller than 10.")
else:                  # La clause else ("sinon") l'est aussi.
    print("some_var is indeed 10.")
    
x = 2
# Python a aussi un opérateur conditionnel ternaire, qui peut être utilisé en une seule ligne.
'pair' if x%2==0 else 'impair' #=> pair
some_var is smaller than 10.
Out[2]:
'pair'
In [45]:
#Les boucles "for" itèrent sur une liste
for animal in ["chien", "chat", "souris"]:
    # On peut utiliser format() pour interpoler des chaînes formattées
    print("{} est un mammifère".format(animal))
chien est un mammifère
chat est un mammifère
souris est un mammifère
In [46]:
# "range(nombre)" retourne un itérable de nombres de zéro au nombre donné
for i in range(4):
    print(i)
0
1
2
3
In [47]:
#"range(debut, fin)" retourne un itérable de nombre de debut à fin.
for i in range(4, 8):
    print(i)
4
5
6
7
In [48]:
#"range(debut, fin, pas)" retourne un itérable de nombres
#de début à fin en incrémentant de pas.
#Si le pas n'est pas indiqué, la valeur par défaut est 1.

for i in range(4, 8, 2):
    print(i)
4
6
In [49]:
#Les boucles "while" bouclent jusqu'à ce que la condition devienne fausse.
x = 0
while x < 4:
    print(x)
    x += 1  # Raccourci pour x = x + 1
0
1
2
3
In [50]:
# On gère les exceptions avec un bloc try/except
try:
    # On utilise "raise" pour lever une erreur
    raise IndexError("Ceci est une erreur d'index")
except IndexError as e:
    pass    # Pass signifie simplement "ne rien faire". Généralement, on gère l'erreur ici.
except (TypeError, NameError):
    pass    # Si besoin, on peut aussi gérer plusieurs erreurs en même temps.
else:   # Clause optionelle des blocs try/except. Doit être après tous les except.
    print("Tout va bien!")   # Uniquement si aucune exception n'est levée.
finally: #  Éxécuté dans toutes les circonstances.
    print("On nettoie les ressources ici")
On nettoie les ressources ici
In [54]:
# Python offre une abstraction fondamentale : l'Iterable.
# Un itérable est un objet pouvant être traîté comme une séquence.
# L'objet retourné par la fonction range() est un itérable.

filled_dict = {"one": 1, "two": 2, "three": 3}
our_iterable = filled_dict.keys()
print(our_iterable) #=> range(1,10). C'est un objet qui implémente l'interface Iterable

# On peut boucler dessus
for i in our_iterable:
    print(i)    # Affiche one, two, three
dict_keys(['three', 'one', 'two'])
three
one
two
In [52]:
# Cependant, on ne peut pas accéder aux éléments par leur adresse.
our_iterable[1]  # Lève une TypeError
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-52-6878533980f9> in <module>()
      1 # Cependant, on ne peut pas accéder aux éléments par leur adresse.
----> 2 our_iterable[1]  # Lève une TypeError

TypeError: 'dict_keys' object does not support indexing
In [53]:
# Un itérable est un objet qui sait créer un itérateur.
our_iterator = iter(our_iterable)

# Notre itérateur est un objet qui se rappelle de notre position quand on le traverse.
# On passe à l'élément suivant avec "next()".
next(our_iterator)  #=> "one"

# Il garde son état quand on itère.
next(our_iterator)  #=> "two"
next(our_iterator)  #=> "three"

# Après que l'itérateur a retourné toutes ses données, il lève une exception StopIterator
next(our_iterator) # Lève une StopIteration

# On peut mettre tous les éléments d'un itérateur dans une liste avec list()
list(filled_dict.keys())  #=> Returns ["one", "two", "three"]
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-53-6e8dd51c3168> in <module>()
     11 
     12 # Après que l'itérateur a retourné toutes ses données, il lève une exception StopIterator
---> 13 next(our_iterator) # Lève une StopIteration
     14 
     15 # On peut mettre tous les éléments d'un itérateur dans une liste avec list()

StopIteration: 

Fonctions

In [32]:
# On utilise "def" pour créer des fonctions
def add(x, y):
    print("x est {} et y est {}".format(x, y))
    return x + y    # On retourne une valeur avec return

# Appel d'une fonction avec des arguments :
add(5, 6)   # => affiche "x est 5 et y est 6" et retourne 11

# Une autre manière d'appeler une fonction : avec des arguments nommés.
add(x=5, y=6)
# Les arguments nommés peuvent être indiqués dans n'importe quel ordre 
#(mais doivent toujours être indiqués après les arguments non nommés).
add(y=6, x=5)  
x est 5 et y est 6
x est 5 et y est 6
x est 5 et y est 6
Out[32]:
11
In [39]:
# Un argument est optionnel s'il a une valeur par défaut dans la déclaration de la fonction
def add(x,y=1):
    return x+y
add(3)
Out[39]:
4
In [40]:
# Définir une fonction qui prend un nombre variable d'arguments
def varargs(*args):
    return args

# La variable args fait référence à un tuple des valeurs des arguments. 
varargs(1, 2, 3)   # => (1, 2, 3)

# On peut aussi définir une fonction qui prend un nombre variable d'arguments nommés.
def keyword_args(**kwargs):
    return kwargs

# La variable kwargs fait référence à un dictionnaire des noms et valeurs des arguments. 
keyword_args(big="foot", loch="ness");   # => {"big": "foot", "loch": "ness"}

# On peut aussi faire les deux à la fois :
def all_the_args(*args, **kwargs):
    print(args)
    print(kwargs)
    
all_the_args(1, 2, a=3, b=4)
(1, 2)
{'a': 3, 'b': 4}
In [41]:
# En appelant des fonctions, on peut aussi faire l'inverse :
# utiliser * pour étendre un tuple de paramètres 
# et ** pour étendre un dictionnaire d'arguments.
# On parle d'opérateur de dépaquetage. 
args = (1, 2, 3, 4)
kwargs = {"a": 3, "b": 4}
all_the_args(*args)   # équivalent à foo(1, 2, 3, 4)
all_the_args(**kwargs)   # équivalent à foo(a=3, b=4)
all_the_args(*args, **kwargs)   # équivalent à foo(1, 2, 3, 4, a=3, b=4)
(1, 2, 3, 4)
{}
()
{'a': 3, 'b': 4}
(1, 2, 3, 4)
{'a': 3, 'b': 4}
In [42]:
# On peut retourner plusieurs valeurs (avec un tuple)
def swap(x, y):
    return y, x # Retourne plusieurs valeurs avec un tuple sans parenthèses.
                # (Note: on peut aussi utiliser des parenthèses)

x = 1
y = 2
x, y = swap(x, y) # => x = 2, y = 1
# (x, y) = swap(x,y) # Là aussi, rien ne nous empêche d'ajouter des parenthèses
x,y
Out[42]:
(2, 1)
In [43]:
# Espace de noms.
# Les fonctions ont leur propre espace de nom.
# Toute variable définie dans une fonction est locale à la fonction. 
x = 5

def setX(num):
    # La variable locale x n'est pas la même que la variable globale x
    x = num # => 43
    print (x) # => 43

# On peut exceptionellement changer la valeur d'une variable de l'espace de nom global
# depuis une fonction en utilisant le mot clé global
# C'est en général considéré comme une mauvaise pratique. 
def setGlobalX(num):
    global x
    print (x) # => 5
    x = num # la variable globale x est maintenant 6
    print (x) # => 6

setX(43)
setGlobalX(6)
x
43
5
6
Out[43]:
6
In [6]:
# On peut définir une application partielle avec functools.partial
from functools import partial

def f(x,y):
    return 3*x+2*y
    
f(1,1) # => 5
g = partial(f, y=1) #=> g(x) = f(x,1); g est l'application partielle de f où y est fixé à 1.
g(1) #=> 5
Out[6]:
5
In [44]:
# lambda permet d'écrire des "fonctions anonymes" en une seule ligne. 
(lambda x: x > 2)(3)   # => True
(lambda x, y: x ** 2 + y ** 2)(2, 1) # => 5
Out[44]:
5
In [116]:
# Map permet d'appliquer une fonction à chaque élément d'un itérable et renvoie un itérable
map(add, [1, 2, 3])   # => [11, 12, 13]
map(max, [1, 2, 3], [4, 2, 1])   # => [4, 2, 3]

# Filter permet de filtrer les éléments d'un itérable par une fonction booléeene.
filter(lambda x: x > 5, [3, 4, 5, 6, 7])   # => [6, 7]
Out[116]:
<filter at 0x7f47c48f8898>

Compréhensions de listes

In [186]:
# La compréhension de liste est un élément important du langage Python. 
# Elle permet de faire des map et des filter de façon très intuitive. 

# Prenons une fonction et une liste:
def f(x):
    return 3*x 
a = [1,3,4,20]


# Pour appliquer la fonction aux éléments de la liste on peut faire:
b = []
for x in a:
    b.append(f(x))
    
# ... Ou utiliser une compréhension de liste:
b = [f(x) for x in a]

# ... ou map
b = list(map(f, a))

# Pour appliquer un filtre on peut faire 
b = []
for x in a:
    if x%2==0:
        b.append(x)
        
# ...ou utiliser une compréhension de liste
b = [x for x in a if x%2==0]

# ...ou un filter
b = list(filter(lambda x: x%2==0, a))

# On peut aussi combiner les deux:
b = [f(x) for x in a if x%2==0]
b = list(map(f, filter(lambda x: x%2==0, a)))
In [189]:
# On peut aussi faire des compréhension de dictionnaires:
b = {x*2:x for x in a}
c = {k:v**2 for k,v in b.items()}

Classes

In [48]:
# On utilise l'opérateur "class" pour définir une classe d'objet.
class Human:

    # Un attribut de la classe. Il est partagé par toutes les instances de la classe.
    species = "H. sapiens"

    # L'initialiseur de base. Il est appelé quand la classe est instanciée.
    # Note : les doubles underscores au début et à la fin sont utilisés pour
    # les fonctions et attributs utilisés par Python mais contrôlés par l'utilisateur.
    # Les méthodes (ou objets ou attributs) comme: __init__, __str__,
    # __repr__ etc. sont appelés méthodes magiques.
    # Vous ne devriez pas inventer de noms de ce style.
    def __init__(self, name):
        # Assigner l'argument à l'attribut de l'instance
        self.name = name

    # Une méthode de l'instance. Toutes prennent "self" comme premier argument.
    def say(self, msg):
        return "{name}: {message}".format(name=self.name, message=msg)

# Instantier une classe signifie créer un objet de cette classe. 
i = Human(name="Ian")
print(i.say("hi"))     # affiche "Ian: hi"

j = Human("Joel")
print(j.say("hello"))  # affiche "Joel: hello"
Ian: hi
Joel: hello

Modules

In [74]:
# On peut importer des modules
import math
print(math.sqrt(16))  # => 4.0

# On peut importer des fonctions spécifiques d'un module
from math import ceil, floor
print(ceil(3.7))  # => 4.0
print(floor(3.7))   # => 3.0

# On peut importer toutes les fonctions d'un module
# Attention: ce n'est pas recommandé.
from math import *

# On peut raccourcir un nom de module
import math as m
math.sqrt(16) == m.sqrt(16)   # => True

# Les modules Python sont juste des fichiers Python.
# Vous pouvez écrire les vôtres et les importer. Le nom du module
# est le nom du fichier.

# On peut voir quels fonctions et objets un module définit
import math
#dir(math)
4.0
4
3

Fichiers

In [130]:
# Lire un fichier avec open renvoie un objet file:
handler = open('file.txt', 'r')

# Le premier argument est le chemin vers le fichier.
# Le second est le mode r:lecture, w:écriture, a: ajout. (rb, wb, ab pour lire/écrire directement en binaire)
# FileNotFoundError est levée si le fichier n'exsite pas en mode 'r'.
open('non_existing_file.txt', 'r') #=> FileNotFoundError
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
<ipython-input-130-619f7287462d> in <module>()
      4 # Le second est le mode r:lecture, w:écriture, a: ajout. (rb, wb, ab pour lire/écrire directement en binaire)
      5 # FileNotFoundError est levée si le fichier n'exsite pas en mode 'r'.
----> 6 open('non_existing_file.txt', 'r')

FileNotFoundError: [Errno 2] No such file or directory: 'non_existing_file.txt'
In [ ]:
# Un fichier ouvert avec open doit être fermé avec .close.
handler = open('test.txt', 'r')
handler.read() #=> Première ligne\nDeuxième ligne"
handler.close()
handler.read() #=> Lire un fichier fermé lève une ValueError

# Pour ne pas oublier de fermer les fichier il est recommandé d'utiliser un bloc with
with open('test.txt','r') as handler:
    handler.read()
#=> close est appelé automatiquement à la fin du bloc. 
handler.read() #=> ValueError

# Itérer sur les lignes d'un fichier
with open('test.txt','r') as handler:
    for line in handler:
        print(line)
In [133]:
# Écrire dans un fichier:
with open('new.txt','w') as handler:
    handler.write('Première ligne\n')

with open('new.txt','w') as handler:
    handler.write('En mode "w", le fichier est écrasé\n')
    
# Ajouter à la fin d'un fichier
with open('new.txt','a') as handler:
    handler.write('Seconde ligne')

open('new.txt','r').read(); #=> 'En mode "w", le fichier est écrasé\nSeconde ligne'

Expressions Régulières

Les Expressions Régulières (ou regexp voire RE) constituent un petit langage de programmation qui permet de manipuler des chaînes de caractères. Référence: How To Regex (Python documentation), xkcd 208, xkcd 1313.

In [6]:
# Les regexp font partie de la bibliothèque standard de Python. Il n'y a pas de module à installer.
import re 
In [13]:
# Deux interfaces sont disponibles:
poeme = """Ce que l’on conçoit bien s’énonce clairement,
Et les mots pour le dire arrivent aisément"""

# Fonctionnel
# Est-ce que la chaine commencer par le pattern ?
re.match('mot', poeme) #=> None, match ne cherche à matcher le pattern que dans 
# Est-ce que le pattern se trouve dans la chaine ? (Retourne la position de occurence)
re.search('mot', poeme) #=> Match object 
# Renvoyer toutes les occurences du pattern dans la chaine sous forme de chaines.
re.findall('mot', poeme) #=> ['mot']
# Trouver toutes les occurences du pattern dans la chaine sous.
re.finditer('mot', poeme) #=> iterable of match object
# Séparer la chaine à chaque occurence du pattern
re.split('mot', poeme) #=> ['ce que...', 'pour le dire...']
# Remplacer toutes les occurences du pattern
re.sub('mot', 'terme', poeme) #=> '(...) Et les termes pour le dire (...)'

# Objet:
regexp = re.compile('mot')
regexp.match(poeme)
regexp.search(poeme)
regexp.findall(poeme)
regexp.finditer(poeme)
regexp.split(poeme)
regexp.sub('terme', poeme);
# Les diffèrences de performances sont négligeables car toutes les regexp sont mises
# en cache quelque soit l'inteface utilisée.
Caractère Signification
. Tout caractère (sauf \n)
[abc] a,b ou c
[a-z] abcdefghijklmopqrstuvwyz
[^abc] tout sauf a,b ou c
\[ [
A ⎮ B A ou B
Position
^ Début de la chaîne
$ Fin de la chaîne
Répétition (du caractère juste avant)
* Répété 0+ fois
+ Répété 1+ fois
'?` Répété 0 ou 1 fois
{m} Répété m fois
{m,n} Répété entre m et n fois
Groupes
(blah) définit un groupe pour l'objet match
In [ ]:
# L'objet Match 
match.groups()  # un n-uplet contenant tous les sous groupes d'un match
match.start(group) # Position du début groupe n.(le groupe 0 est tout le match)
match.end(group) # Position de la fin du groupe n.
match.span(group) # (debut, fin)

Calcul numérique avec Numpy

Numpy est la bibliothèque de calcul numérique qui implémente un objet très pratique: le tableau n-dimensionel (np.array).

N'hésitez pas à lire le tutorial numpy en entier. Gardez la liste des fonctions de la bibliothèque sous le coude.

In [50]:
# Il est d'usage de l'importer sous le nom np (en utilisant l'operateur `as`)
import numpy as np

L'objet principal de numpy est le tableau (np.array). Ces valeurs sont stockées en continu dans la mémoire de l'ordinateur, ce qui les rend bien plus rapide que les listes python pour de nombreuses opérations.

Un tableau numpy a 4 attributs importants:

  • np.array.ndim : le nombre de dimensions du tableau (axes)
  • np.array.shape : la taille du tableau le long de chaque axe. (2,3) signifie 2 lignes, 3 colonnes.
  • np.array.size: le nombre total d'éléments du tableau.
  • np.array.dtype: Le type des éléments du tableau, en général des nombres à virgule flotante sur 64 bit (np.float64) mais ça peut être des entiers sur 32 (np.int32) ou des nombres complexes (np.complex) voire des objets (objects)

Pour instancier un tableau vous pouvez utiliser:

  • np.array(iterateur) pour l'initialiser avec une séquence de votre choix.
  • np.zeros(shape) pour initialiser un tableau de zéros.
  • np.arange(debut,fin,pas) pour une séquence arithmétique d'entiers.
  • np.linspace(deut,fin,nombre_de_pas) pour une séquence artihmétique de nombre à virgule flotante.
In [122]:
# Examples
a = np.array([1,2,3,4])
b = np.array([(1.5,2,3), (4,5,6)])
c = np.array( [ [1,2], [3,4] ], dtype=complex )
d = np.zeros((3,3))
e = np.arange(10)
f = np.linspace(0,1,10)

for elt in (a,b,c,d,e,f):
    print(elt, end='\n\n')
[1 2 3 4]

[[1.5 2.  3. ]
 [4.  5.  6. ]]

[[1.+0.j 2.+0.j]
 [3.+0.j 4.+0.j]]

[[0. 0. 0.]
 [0. 0. 0.]
 [0. 0. 0.]]

[0 1 2 3 4 5 6 7 8 9]

[0.         0.11111111 0.22222222 0.33333333 0.44444444 0.55555556
 0.66666667 0.77777778 0.88888889 1.        ]

In [118]:
# Les opérations aritmhétiques: `+, -, *, ** , <, >` sont appliquées élément par élément. 
a = np.array([1,2,3])
print(a)
print(a + a)
print(a - a)
print(a * a)
print(a**2)
print(a>1)
[1 2 3]
[2 4 6]
[0 0 0]
[1 4 9]
[1 4 9]
[False  True  True]

On peut sommer ou multiplier les tableau avec des scalaires, l'opération se fait élément par élément (on parle de broadcasting)

In [119]:
a = np.array([4,2,3])
print(a + 10)
print(a * 3)
[14 12 13]
[12  6  9]

Les np.arrays ont des méthodes prédéfinies pour le min, max, sum...

In [54]:
a = np.arange(10)
print(a.max())
print(a.min())
print(a.sum())
9
0
45

Quelques méthodes et fonctions d'algèbre linéaire:

In [56]:
A = np.array([[1.0, 2.0], [3.0, 4.0]])
A.transpose() # transposée
np.dot(A,A) # produit matriciel

np.linalg.inv(A) # Inverse
np.trace(A) # trace
np.linalg.eig(A) # valeurs propres et vecteurs propres associées. 

print(A)
[[1. 2.]
 [3. 4.]]

L'indexage se fait comme pour les listes python: debut:fin:pas, sauf que l'on peut avoir plusieurs axes:

In [17]:
B = np.eye(4,4)
B[2,3]
B[0:5, 1]                      
B[ : ,1]                        
B[1:3, : ]

B[ : ,1] == B[ 0:, 1] 
B[ : ,1] ==  B[ :4, 1] 
B[ : ,1] ==  B[ 0:4, 1];

Graphes avec Matplotlib

Matplotlib est la bibliothèque de grapheur la plus utilisée dans la communauté Python. Encore une fois, si vous voulez en savoir plus allez voir le tutorial officiel.

In [78]:
# Matplotlib est la bibliothèque qui s'occupe de tracer des graphes. 
# Son module pyplot offre une interface simple et fonctionnelle. 
# Il est d'usage de l'importer sous le nom plt. 
import matplotlib.pyplot as plt

# Cette commande (spécifique à jupyter) permet d'afficher les graphes de matplotlib directement dans 
# le navigateur. 
%matplotlib inline
# Alternativement, utilisez `notebook` plutôt que `inline` pour une version interactive.
#%matplotlib notebook
In [59]:
# plot permet de tracer des courbes par interpolation linéaire. 
x = np.linspace(0,3*np.pi)
y = np.sin(x) 
plt.plot(x,y)
Out[59]:
[<matplotlib.lines.Line2D at 0x7f47aaf01978>]
In [21]:
# scatter permet de tracer des points. 
plt.scatter(x,y)
Out[21]:
<matplotlib.collections.PathCollection at 0x7f433e9602e8>
In [22]:
# hist permet de tracer des histogrammes. 
plt.hist(y)
Out[22]:
(array([ 6.,  4.,  2.,  2.,  4.,  4.,  4.,  6.,  4., 14.]),
 array([-0.99537911, -0.79589258, -0.59640605, -0.39691951, -0.19743298,
         0.00205355,  0.20154008,  0.40102662,  0.60051315,  0.79999968,
         0.99948622]),
 <a list of 10 Patch objects>)
In [60]:
# Il y a deux types d'objets principaux en matplotlib: les figures et les axes (ou sous-figures).

# Par défaut, les plots sont fait sur l'objet axe courant (plt.gca()).
# On peut changer l'objet axe courant avec plt.subplot(position) ou
# (plus pratique) on peut assigner tous les objets axes d'un coup:
fig, (ax1,ax2,ax3) = plt.subplots(1,3, figsize=(12,4))
ax1.plot(x,y)
ax2.scatter(x,y)
ax3.hist(y)

# La méthode set des objets axe permet de donner un titre ou de changer
# les propriétés du graphe (limites de la zone à tracer, graduations...)
ax1.set(xlabel='titre x', ylabel='titre y', title='titre du graphe')
ax2.set(xlim=(-1,15))
ax3.set(xticks=(-1,0,.7,1))

# La méthode savefig de l'objet figure permet de sauvegarder l'image dans un fichier.
fig.savefig('test.png')

Calcul scientifique avec Scipy

La bibliothèque scipy (scientific python) contient toute une foule de fonctions utilie pour le calcul numérique. En voici quelques morceaux choisis. N'oubliez pas de vous référer à la documentation pour plus de détails.

Intégration numérique

In [51]:
# Le module scipy.integrate permet de réaliser de l'intégration numérique.
import scipy.integrate

$$\int_0^1 f(x) dx$$

In [109]:
# Calcul de l'intégrale d'une fonction sur un intervalle
def f(x):
    return 4*np.sqrt(1-x**2)

scipy.integrate.quad(f, 0, 1)
Out[109]:
$$\left ( 3.1415926535897922, \quad 3.533564552071766e-10\right )$$

$$ y(t) = y(0) + \int_0^t f(y(t)) $$

In [115]:
# Intégration numérique d'une équation différentielle
def dxdt(x, t):
    return 3*x
y0 = 1 
traj = scipy.integrate.odeint(dxdt, y0, np.linspace(0,1))
plt.plot(traj)
Out[115]:
[<matplotlib.lines.Line2D at 0x7f47c48d3be0>]

Statistiques

In [52]:
# Le module scipy.stats contient de nombreuses distributions de probabilités et des tests statistiques.
import scipy.stats
In [74]:
X = scipy.stats.norm(loc=0) # Une variable aléatoire normalement distribuée centrée réduite 
X.mean() #=> 0
X.std() #=> 1 
X.pdf(1) #=> \phi(1)

Y = scipy.stats.norm(loc=10) # v.a. gaussienne, espérance 10
Z = scipy.stats.norm(loc=10, scale=.3) # v.a. gaussienne, espérance 10, ecart-type .3
In [75]:
# Échantilloner une variable aléatoire:
sampleX = X.rvs(size=100)
sampleY = Y.rvs(size=100)
sampleZ = Z.rvs(size=100)
In [94]:
x = np.linspace(-10,12,100)

# rv.pdf() donne la fonction densité de probabilité
plt.plot(x, X.pdf(x))
plt.plot(x, Y.pdf(x))
plt.plot(x, Z.pdf(x))

plt.hist(sampleX, alpha=.5, label='X', normed=True, color='C0')
plt.hist(sampleY, alpha=.5, label='Y', normed=True, color='C1')
plt.hist(sampleZ, alpha=.5, label='Z', normed=True, color='C2')
plt.legend();
/home/guilhem/.local/lib/python3.5/site-packages/matplotlib/axes/_axes.py:6499: MatplotlibDeprecationWarning: 
The 'normed' kwarg was deprecated in Matplotlib 2.1 and will be removed in 3.1. Use 'density' instead.
  alternative="'density'", removal="3.1")
In [98]:
# T-test à deux échantillons
scipy.stats.ttest_ind(sampleX, sampleY) # => pvalue < 0.05
scipy.stats.ttest_ind(sampleY, sampleZ) # => pvalue > 0.05
Out[98]:
Ttest_indResult(statistic=-0.18557892843846313, pvalue=0.8529649502348823)

Calcul symbolique avec Sympy

Sympy est une bibliothèque de calcul symbolique en Python. Elle permet de résoudre un grand nombre de problèmes courants en calcul scientifique: simplifications, résolution d'équations, différentiation, calcul de limites... Lisez la documentation pour plus d'informations.

In [34]:
import sympy
sympy.init_printing() # Pretty_print permet d'afficher les équations dans le notebook via LaTeX. 
In [35]:
# Tout d'abord il faut créer un objet symbole pour chaque grandeur utilisée.
x,y, a, b, c = sympy.symbols('x,y,a,b,c')

# On peut ensuite créer des expressions en utilisant les opérateurs usuels de python.
expr = a * (x + b)
expr
Out[35]:
$$a \left(b + x\right)$$
In [79]:
# .expand permet de développer une expression
expr.expand()
Out[79]:
$$a b + a x$$
In [92]:
# .simplify permet de la simplifier
(a + (-a + b * 1/2 * c )+ b).simplify()

# La simplification par défaut n'est pas toujours satisfaisante. 
# voir https://docs.sympy.org/latest/tutorial/simplification.html
Out[92]:
$$\frac{b \left(c + 2\right)}{2}$$
In [93]:
# Créer une equation se fait avec l'objet Eq.
eq = sympy.Eq(a*x+b, 0)
eq 
Out[93]:
$$a x + b = 0$$
In [94]:
# Résoudre une équation pour une variable donnée:
sympy.solve(eq, x)
Out[94]:
$$\left [ - \frac{b}{a}\right ]$$
In [95]:
# Dériver une expression
sympy.diff(expr, x)
Out[95]:
$$a$$
In [49]:
expr = 4*sympy.sqrt(1-x**2)
expr
Out[49]:
$$4 \sqrt{- x^{2} + 1}$$
In [50]:
# Calcul de l'intégrale d'une fonction sur un intervalle
sympy.integrate(expr,(x,0,1))
Out[50]:
$$\pi$$

Tableaux de données avec Pandas

Pandas est une bibliothèque qui implémente la classe "tableau" de donnée (pandas.Dataframe). Il s'agit d'un tableau a double entrée constitué de colonnes (pandas.Series) qui peuvent être de différent types. Lisez la documentation pour plus d'information. Le tutoriel pandas en 10 minutes est très pratique.

In [123]:
import pandas as pd
In [136]:
# On peut initialiser un pd.Dataframe avec un np.Array.
data = pd.DataFrame(np.random.random(size=(10,3)), columns=['A','B','C'])

# Pandas possède de nombreuses fonctions pour importer des données depuis un fichier:
try:
    pd.read_csv("data.csv")
    pd.read_csv("data.tsv", sep='\t')
    pd.read_json("data.json")
    pd.read_excel("data.xls")
except FileNotFoundError:
    pass
data
Out[136]:
A B C
0 0.179734 0.702595 0.874070
1 0.495800 0.407448 0.249665
2 0.336099 0.443157 0.926575
3 0.957757 0.236765 0.996101
4 0.961121 0.784629 0.154384
5 0.120409 0.383636 0.510969
6 0.505870 0.268738 0.269260
7 0.759919 0.004244 0.521494
8 0.061665 0.277545 0.517106
9 0.940788 0.698134 0.667816
In [137]:
# Dataframe.describe() donne quelques statistiques descriptives.
data.describe()
Out[137]:
A B C
count 10.000000 10.000000 10.000000
mean 0.531916 0.420689 0.568744
std 0.355833 0.245354 0.295362
min 0.061665 0.004244 0.154384
25% 0.218825 0.270940 0.329687
50% 0.500835 0.395542 0.519300
75% 0.895571 0.634390 0.822506
max 0.961121 0.784629 0.996101
In [138]:
# L'indexage avancé permet de filtrer les tableaux avec des booléens. 
data[data.A>0.5]
Out[138]:
A B C
3 0.957757 0.236765 0.996101
4 0.961121 0.784629 0.154384
6 0.505870 0.268738 0.269260
7 0.759919 0.004244 0.521494
9 0.940788 0.698134 0.667816
In [143]:
data['GrandA'] = ['Oui' if x>0.5 else 'Non' for x in data.A]
data
Out[143]:
A B C GrandA
0 0.179734 0.702595 0.874070 Non
1 0.495800 0.407448 0.249665 Non
2 0.336099 0.443157 0.926575 Non
3 0.957757 0.236765 0.996101 Oui
4 0.961121 0.784629 0.154384 Oui
5 0.120409 0.383636 0.510969 Non
6 0.505870 0.268738 0.269260 Oui
7 0.759919 0.004244 0.521494 Oui
8 0.061665 0.277545 0.517106 Non
9 0.940788 0.698134 0.667816 Oui
In [148]:
# Dataframe.groupby permet de grouper les valeurs en fonction de la valeur d'une colonne.
# Cela donne un objet groupby qui a ses méthodes mean, max, sum...
data.groupby('GrandA').mean()
Out[148]:
A B C
GrandA
Non 0.238742 0.442876 0.615677
Oui 0.825091 0.398502 0.521811
In [154]:
# Et sur lequel on peut itérer:
for value, dt in data.groupby('GrandA'):
    print('Le groupe "{}" a {} valeurs.'.format(value, dt.shape[0]))
Le groupe "Non" a 5 valeurs.
Le groupe "Oui" a 5 valeurs.
In [162]:
# Pandas est compatible avec matplotlib...
plt.scatter(data.A, data.B);

#...et les dataframes possèdent une méthode permettant des visualisations rapides.
data.plot()
Out[162]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f478c47ca20>
In [175]:
# Les dataframe permettent des opérations sophistiquées de bases de données comme la jointure de table.
etudiants = pd.DataFrame([{"Nom":"Alice","Parcours":"Biologie", "Niveau":"L3"},
                         {"Nom":"Bob","Parcours":"Biologie", "Niveau":"L3"},
                         {"Nom":"Megan","Parcours":"Biologie", "Niveau":"M1"}])
notes = pd.DataFrame({'Nom':['Alice','Alice','Bob','Bob','Megan', 'Megan'],
                      'Cours':['Python','R']*3,
                      'Note':[15,12,10,16,18,11]})

from IPython.display import display
display(etudiants, notes)
pd.merge(etudiants, notes, left_on='Nom', right_on='Nom')
Niveau Nom Parcours
0 L3 Alice Biologie
1 L3 Bob Biologie
2 M1 Megan Biologie
Cours Nom Note
0 Python Alice 15
1 R Alice 12
2 Python Bob 10
3 R Bob 16
4 Python Megan 18
5 R Megan 11
Out[175]:
Niveau Nom Parcours Cours Note
0 L3 Alice Biologie Python 15
1 L3 Alice Biologie R 12
2 L3 Bob Biologie Python 10
3 L3 Bob Biologie R 16
4 M1 Megan Biologie Python 18
5 M1 Megan Biologie R 11