Opérations, contrôle, fonctions et modules

  • Opérateurs
  • Structures de contrôle
  • Fonctions
  • Exceptions & gestionnaires de contexte
  • Compréhensions de listes & expressions génératrices
  • Modules
  • Bonnes pratiques

Contenu sous licence CC BY-SA 4.0

Opérateurs

Arithmétiques

+, -, *, /, //, %, **

sont des opérateurs classiques qui se comportent de façon habituelle.

Particularités de la division

In [1]:
# Avec des nombres entiers
print(16 / 3)  # Quotient de la division euclidienne (produit un réel)
print(16 // 3) # Quotient de la division euclidienne (produit un entier)
print(16 % 3)  # Reste de la division euclidienne (produit un entier)

# Avec des nombres flottants
print(16. / 3)  # Division (produit un réel)
print(16. // 3) # Quotient de la division (produit un réel)
print(16. % 3)  # Reste de la division ou modulo (produit un réel)
5.333333333333333
5
1
5.333333333333333
5.0
1.0

Puissance

In [2]:
print(2**10)
# On peut aussi utiliser la fonction pow() du module math, mais celle-ci renvoie un réel.
import math
print(math.pow(2, 10))
1024
1024.0

Logiques

and, or, not

retournent une valeur booléenne.

In [3]:
print(True or False)
print(True and False)
print(not True)
print(not False)
print(not [])
print(not (1, 2, 3))
True
False
False
True
True
False

Attention, ce sont des opérateurs court-circuit :

In [4]:
a = True
b = False and a  # b vaut False sans que a soit évalué
c = True or a    # c vaut True, sans que a soit évalué

Pour s'en convaincre :

In [5]:
True or print("nicht a kurz schluss")
False and print("not a short circuit")
print('on a prouvé que ce sont des opérateurs "court-circuit"...')
on a prouvé que ce sont des opérateurs "court-circuit"...

Exercice : Modifiez les valeurs True et False dans la cellule précédente, pour visualiser le fonctionnement de ces opérateurs.

Comparaison

==, is, !=, is not, >, >=, <, <=

L'évaluation de ces opérateurs retourne une valeur booléenne.

In [6]:
print(2 == 2)
print(2 != 2)
print(2 == 2.0)
print(type(2) is int)
True
False
True
True

On peut utiliser ces opérateurs avec des variables et des appels à des fonctions.

In [7]:
x = 3
print(1 > x)
y = [0, 1, 42, 0]
print(x <= max(y))
print(x <= min(y))
False
True
False

On peut chaîner ces opérateurs, mais ils fonctionnent en mode court-circuit et l'opérande centrale n'est évaluée qu'une seule fois.

In [8]:
x = 3
print(2 < x <= 9) # équivalent à 2 < x and x <= 9
True

Attention : comparer des types non numériques peut avoir des résultats surprenants.

In [9]:
# Chaînes de caractères
print("aaa" < "abc")
print("aaa" < "aaaa")
print("22" > "3")
True
True
False
In [10]:
# Listes
print([1, 2, 3, 4] > [42, 42])
print([666] > [42, 42])
False
True

Attention : comparer des types incompatibles peut avoir des résultats surprenants.

In [11]:
# Cette cellule génère des erreurs
print('chaîne:\t', "a" < 2)
print('liste:\t', ["zogzog"] > 42)
print('vide:\t', [] > 1)
print('tuple:\t', [23, 24] >= (23, 24))
print('dict:\t', [23, 24] >= {23: True, 24: "c'est pas faux"})
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[11], line 2
      1 # Cette cellule génère des erreurs
----> 2 print('chaîne:\t', "a" < 2)
      3 print('liste:\t', ["zogzog"] > 42)
      4 print('vide:\t', [] > 1)

TypeError: '<' not supported between instances of 'str' and 'int'

Attention, l'égalité de valeur n'implique pas forcément que l'identité des objets comparés est la même.

In [12]:
a = []  # a est une liste vide
b = []  # b est une AUTRE liste vide

print(a == b) # test de valeur
print(a is b) # test d'identité
print(f'{id(a) = }')
print(f'{id(b) = }')
True
False
id(a) = 140474522237440
id(b) = 140474522238208

Mais, comme vu précédemment, des variables différentes peuvent référencer le même objet.

In [13]:
c = a
print(a == c) # test de valeur
print(a is c) # test d'identité
True
True

bits à bits (bitwise)

|, ^, &, <<, >>, ~
  • Ces opérateurs permettent de manipuler individuellement les bits d'un entier.
  • Ce sont des opérations bas-niveau, souvent utilisées pour piloter directement du matériel, pour implémenter des protocoles de communication binaires (par exemple réseau ou disque).
  • L'utilisation d'entiers comme ensemble de bits permet des encodages de données très compacts, un booléen (True, False) ne prendrait qu'un bit en mémoire, c'est a dire que l'on peut encoder 64 booléens dans un entier.

Description complète ici.

In [14]:
val = 67 # == 64 + 2 + 1 == 2**6 + 2**1 + 2**0 == 0b1000011
print(bin(val))

mask = 1 << 0 # On veut récupérer le 1er bit
print('le 1er  bit vaut', (val & mask) >> 0)

mask = 1 << 1 # On veut récupérer le 2ème bit
print('le 2ème bit vaut', (val & mask) >> 1)

mask = 1 << 2 # On veut récupérer le 3ème bit
print('le 3ème bit vaut', (val & mask) >> 2)

mask = 1 << 6 # On veut récupérer le 7ème bit
print('le 7ème bit vaut', (val & mask) >> 6)

# Si on positionne le 4ème bit a 1 (on rajoute 2**3 = 8)
newval = val | (1 << 3)
print(newval)

# Si on positionne le 6ème bit a 0 (on soustrait 2**7 = 64)
print(newval & ~(1 << 6))
0b1000011
le 1er  bit vaut 1
le 2ème bit vaut 1
le 3ème bit vaut 0
le 7ème bit vaut 1
75
11

Exercice : Retournez une chaîne de caractères représentant le nombre contenu dans x écrit en notation binaire. Par exemple :

$$ 5 \rightarrow 101 \\ 6 \rightarrow 110 \\ 7 \rightarrow 111 $$

In [15]:
x = 7
# votre code ici
In [16]:
# Décommentez puis exécutez pour afficher le corrigé :
#%load exos/snippets/bits_a_bits.py

Affectation augmentée

+=, -=, *=, /=, **=  # etc.
In [17]:
a = 4
a += 1  # <=> a = a + 1
print(a)
a //= 2
print(a)
a **= 3
print(a)
a %= 2
print(a)
5
2
8
0

Compatibilité de type, coercition de type

Python effectue certaines conversions implicites quand on ne perd pas d'information (par exemple d'entier vers flottant).

In [18]:
print(1 + 0b1)
print(1 + 1.0)
print(1.0 + 2 + 0b11 + 4j)
2
2.0
(6+4j)

Mais dans d'autres cas, la conversion doit être explicite.

In [19]:
# Cette cellule génère une erreur
a = 1
b = '1'
a + b
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[19], line 4
      2 a = 1
      3 b = '1'
----> 4 a + b

TypeError: unsupported operand type(s) for +: 'int' and 'str'

Exercice :

Sans toucher au +, corrigez la ligne 4 de la celulle ci-dessus afin d'afficher :

  1. la chaîne '11'
  2. l'entier 2
  3. l'entier 11
In [20]:
# Décommentez puis exécutez pour afficher le corrigé :
#%load exos/snippets/compatibilite.py

Priorité des opérateurs

Les opérateurs ont des priorités classiques en python.

Par exemple, dans l'ordre :

  1. puissance : **
  2. multiplication, division : * et /
  3. addition, soustraction : + et -

etc.

Utilisez des parenthèses quand elles aident à la lisibilité et à la clarté.

Pour plus d'informations, voir ici.

Structures de contrôle

  • La mise en page comme syntaxe
  • pass
  • tests conditionels : if/elif/else
  • boucles
    • for <element> in <iterable>
    • while
    • break
    • continue

La mise en page comme syntaxe

  • La mise en page est importante en python, c'est une différence majeure avec les autres langages (Java, C/C++, etc.)
  • Python utilise l'indentation du code avec des caractères blancs plutôt que des mots clés (begin/end en pascal) ou des symboles ({} en java et C/C++). Cela permet de rendre le code plus compact.
  • Elle va servir à délimiter des blocs de code sur lesquels les structures de contrôle comme les boucles ou les tests de conditions vont s'appliquer.
  • De toute façon, dans les autres langages, on indente aussi le code pour l'aspect visuel et la lisibilité.
  • L'indentation faisant partie de la syntaxe du langage, il faut être rigoureux et suivre la règle suivante :
    • 4 espaces pour passer au niveau suivant
    • éviter les tabulations et préférer les espaces et surtout ne pas les mélanger !
In [21]:
if True:
    print("toutes")
    print("les")
    print("lignes")
    print("au même niveau d'indentation forment un bloc de code")
print('et quand on remonte, on "termine" un bloc de code')
toutes
les
lignes
au même niveau d'indentation forment un bloc de code
et quand on remonte, on "termine" un bloc de code

Exercice : changez le True en False, et observez quelles lignes de code ne sont plus exécutées.

pass

En cas de besoin d'un bloc de code qui ne fait rien, on utilise le mot clé pass (équivalent à NOP ou NO-OP)

Exemple : une boucle infinie

while True:
    pass

(à ne pas exécuter dans une cellule sous peine de bloquer le noyau de ce notebook)

Tests conditionnels

Les instructions if/elif/else permettent d'exécuter des blocs d'instructions en fonction de conditions :

if test1:
    <bloc d instructions 1>
elif test2:
    <bloc d instructions 2>
else:
    <bloc d instructions 3>

elif est la contraction de "else if".

In [22]:
if True:
    print("c'est vrai!")
c'est vrai!
In [23]:
if False:
    print("je suis caché!")
else:
    print("mais moi je suis en pleine lumière...")
mais moi je suis en pleine lumière...
In [24]:
# Pour cet exemple, on itère sur les éléments d'un tuple (cf. boucle for plus loin)
for position in 2, 9, 3, 1, 8:
    if position == 1:
        print(position, "Or")
    elif position == 2:
        print(position, "Argent")
    elif position == 3:
        print(position, "Bronze")
    else:
        print(position, "Vestiaires")
2 Argent
9 Vestiaires
3 Bronze
1 Or
8 Vestiaires
In [25]:
taille = 1.2
if taille >= 1.70:  # La taille moyenne en France
    print('grand')
else:
    print('petit')
petit

Exercices :

  1. Editez la cellule pour y mettre votre taille et exécutez-la pour savoir si vous êtes grand ou petit.
  2. Gérez le cas des gens de taille moyenne.
In [26]:
# Décommentez puis exécutez pour afficher le corrigé :
#%load exos/snippets/condition.py

Boucles

Les boucles sont les structures de contrôle permettant de répéter l'exécution d'un bloc de code plusieurs fois.

Boucle while

La plus simple est la boucle de type while :

while <condition>:
    <bloc 1>
<bloc 2>

Tant que <condition> est True, le <bloc 1> est exécuté, quand la condition passe à False, l'exécution continue au <bloc 2>.

In [27]:
compteur = 3
while compteur > 0:
    print('le compteur vaut :', compteur)
    compteur -= 1
print('le compteur a été décrémenté 3 fois et vaut maintenant', compteur)
le compteur vaut : 3
le compteur vaut : 2
le compteur vaut : 1
le compteur a été décrémenté 3 fois et vaut maintenant 0

Exercice :
C'est la soirée du réveillon. Ecrivez une boucle while qui décompte les secondes à partir de 5 pour souhaiter la bonne année.
Allez voir par ici pour passer le temps...

In [28]:
import time
# Votre code ici
In [29]:
# Décommentez puis exécutez pour afficher le corrigé :
#%load exos/snippets/while.py

Boucle for

Une boucle plus complexe : for/in

for <variable> in <iterable>:
    <bloc 1>
<bloc 2>

A chaque tour de boucle, la variable <variable> va référencer un des éléménts de l'<iterable>. La boucle s'arrête quand tous les éléménts de l'itérable ont été traités. Il est fortement déconseillé de modifier l'itérable en question dans le <bloc 1>.

In [30]:
invites = ('Aline', 'Bernard', 'Céline', 'Dédé')
for invite in invites:
    print(f'Bonjour {invite}, bienvenue à la soirée de gala !')
print('Maintenant, tout le monde a été bien accueilli.')
Bonjour Aline, bienvenue à la soirée de gala !
Bonjour Bernard, bienvenue à la soirée de gala !
Bonjour Céline, bienvenue à la soirée de gala !
Bonjour Dédé, bienvenue à la soirée de gala !
Maintenant, tout le monde a été bien accueilli.

Exercice : Rajoutez des invités à la fête. Vérifiez que tout le monde est accueilli correctement.

In [31]:
# Maintenant, si nous avons reçu des réponses à notre invitation, utilisons un dictionnaire :
invites = {'Aline': True, 'Bernard': False, 'Céline': True, 'Dédé': True}
for (personne, presence) in invites.items():
    if presence:
        print(f'Bonjour {personne}, bienvenue à la soirée de gala !')
    else:
        print(f'Malheureusement, {personne} ne sera pas avec nous ce soir.')
print('Maintenant, tout le monde a été bien accueilli ou excusé.')
Bonjour Aline, bienvenue à la soirée de gala !
Malheureusement, Bernard ne sera pas avec nous ce soir.
Bonjour Céline, bienvenue à la soirée de gala !
Bonjour Dédé, bienvenue à la soirée de gala !
Maintenant, tout le monde a été bien accueilli ou excusé.

La méthode .items() renvoie une vue itérable des éléments du dictionnaire.

Exercice : Rajoutez des invités à la fête. Certains ayant répondu qu'ils ne pourraient pas venir. Vérifiez que tout le monde est accueilli ou excusé correctement.

Si on souhaite itérer sur une liste et que l'on a besoin de l'indice de ses éléments, on utilise une combinaison de range() et len().

In [32]:
nombres = [2, 4, 8, 6, 8, 1, 0, 1j]
for i in range(len(nombres)):
    nombres[i] **= 2

print("valeurs de i:", list(range(len(nombres))))
print("carrés      :", nombres)
valeurs de i: [0, 1, 2, 3, 4, 5, 6, 7]
carrés      : [4, 16, 64, 36, 64, 1, 0, (-1+0j)]

Il existe une forme raccourcie pour faire ce genre de choses, la fonction interne enumerate()

In [33]:
nombres = [2, 4, 8, 6, 8, 1, 0]
impairs = nombres[:]  # On copie la liste nombres
for (i, nombre) in enumerate(nombres):
    impairs[i] = bool(nombre % 2)
# Les impairs
print(impairs)
[False, False, False, False, False, True, False]

Note

La fonction interne range() retourne un itérateur. C'est un objet qui se comporte comme une liste sans pour autant allouer la mémoire nécessaire au stockage de tous ses éléments. Le coût de création d'une vraie liste augmente avec sa taille (son empreinte mémoire aussi !).

  • une liste (ou un tuple) contient des données
  • un itérateur possède la méthode qui permet de calculer l'élément suivant dans une boucle for.

Note de la note

En Python version 2.x, Il existait deux versions de cette fonctionnalité: range() et xrange(). La première retournait une vraie liste, allouée complètement, alors que xrange() retournait un itérateur.

In [34]:
print(type(range(3)))        # comme xrange() en python2
print(repr(range(3)))        # comme xrange() en python2
print(type(list(range(3))))  # comme range en python2
<class 'range'>
range(0, 3)
<class 'list'>

Instruction break

Il est possible d'arrêter prématurément une boucle grâce a l'instruction break.

L'instruction break est utilisable indifférement dans les boucles for ou while.

In [35]:
compteur = 3
while True:  # Notre boucle infinie
    compteur -= 1
    print('Dans la boucle infinie! compteur =', compteur)
    if compteur <= 0:
        break  # On sort de la boucle while immédiatement
    print('on continue, compteur =', compteur)
print("c'était pas vraiment une boucle infinie...")
Dans la boucle infinie! compteur = 2
on continue, compteur = 2
Dans la boucle infinie! compteur = 1
on continue, compteur = 1
Dans la boucle infinie! compteur = 0
c'était pas vraiment une boucle infinie...

En cas d'imbrication de plusieurs boucles, l'instruction break sort de la plus imbriquée (la plus proche).

In [36]:
for i in (1, 2, 3):
    for j in (1, 2, 3, 4):
        if i == 2:
            break 
        print(f"{i, j = }")
i, j = (1, 1)
i, j = (1, 2)
i, j = (1, 3)
i, j = (1, 4)
i, j = (3, 1)
i, j = (3, 2)
i, j = (3, 3)
i, j = (3, 4)

Instruction continue

Si, dans une boucle, on veut passer immédiatement à l'itération suivante, on utilise l'instruction continue.

In [37]:
compteur = 9
while compteur > 0:
    compteur -= 1
    if compteur % 2:
        compteur /= 2
        print('impair, on divise :', compteur)
        continue  # retourne immédiatement au début de la boucle
    print("pair, RAS")
print("c'est fini...")
pair, RAS
impair, on divise : 3.5
impair, on divise : 1.25
impair, on divise : 0.125
impair, on divise : -0.4375
c'est fini...

Fonctions

Les fonctions permettent de réutiliser des blocs de code à plusieurs endroits différents sans avoir à copier ce bloc.

En python, il n'y a pas de notion de sous-routine. Les procédures sont gérées par les objets de type fonctions, avec ou sans valeur de retour.

def <nom fonction>(arg1, arg2, ...):
    <bloc d instructions>
    return <valeur>  # Instruction optionnelle

On distingue :

  • les fonctions avec return des fonctions sans return
  • les fonctions sans arguments (pour lesquelles () est vide) des fonctions avec arguments (arg1, arg2, ...)

<nom fonction>(arg1, arg2, ...) est appelé signature de la fonction.

Fonctions sans arguments

Fonction sans return

Pour définir une fonction :

In [38]:
def func():  # Definition de la fonction
    print('You know what?')
    print("I'm happy!")

Pour utiliser une fonction que l'on a défini :

In [39]:
func()  # 1er Appel de la fonction
func()  # 2eme appel
func()  # 3eme appel, etc...
print(func())  # On l'appelle et on affiche sa valeur de retour
You know what?
I'm happy!
You know what?
I'm happy!
You know what?
I'm happy!
You know what?
I'm happy!
None

Exercice : Ecrivez une fonction nommée rien qui ne fait rien et appelez là deux fois.

In [40]:
# Votre code ici
In [41]:
# Décommentez puis exécutez pour afficher le corrigé:
#%load exos/snippets/rien.py

Fonction avec return

In [42]:
def func():  # Definition de la fonction
    return "I'm happy"  # La fonction retourne une chaine de caractère

print("1er appel:")
func()  # 1er Appel de la fonction : la valeur retournée n'est pas utilisée
print("2eme appel:")
ret_val = func()  # Le retour du 2eme appel est stocké
print("La fonction func() nous a renvoyé la valeur:", ret_val)
1er appel:
2eme appel:
La fonction func() nous a renvoyé la valeur: I'm happy

Exercice : Ecrivez une fonction nommée eloge_de_rien qui retourne la chaine de caractères rien. Appelez-là et affichez sa valeur de retour.

In [43]:
# Votre code ici
In [44]:
# Décommentez puis exécutez pour afficher le corrigé:
#%load exos/snippets/eloge_de_rien.py

Important : l'instruction return provoque une sortie de la fonction. Dans le code suivant, la ligne qui appelle la fonction print() n'est pas exécutée.

In [45]:
def func():
    return 'je sors'
    print('après return')

func()
Out[45]:
'je sors'

Fonctions avec arguments

Pour définir une fonction qui prend des arguments, on nomme ces arguments entre les parenthèses de la ligne def. Ces paramètres seront définis comme des variables à l'intérieur de la fonction et recevrons les valeurs passées lors des appels de celle-ci.

In [46]:
def somme(x, y):
    return x + y

Pour utiliser cette fonction avec diverses valeurs, il suffit de l'appeler plusieurs fois :

In [47]:
print(somme(1, 2))
print(somme(4, 7))
print(somme(2 + 2, 7)) 
print(somme(somme(2, 2), 7))
3
11
11
11

Exercice :

  • Définissez une fonction nommée chevalier qui prend un paramètre n et qui affiche n fois (avec print()) la chaîne de caractères Nee!
  • appelez cette fonction pour vérifier que chevalier(3) dit bien Nee! trois fois comme il convient !

Voici quelques exemples montrant comment cette fonction doit se comporter:

chevalier(1)
Nee!
chevalier(3)
Nee!Nee!Nee!
chevalier(6)
Nee!Nee!Nee!Nee!Nee!Nee!
In [48]:
def chevalier(n):
    # Votre code ici
    pass
In [49]:
# Vérifions que tout fonctionne bien:
chevalier(1)
chevalier(3)
chevalier(6)
In [50]:
# Décommentez puis exécutez pour afficher le corrigé:
#%load exos/snippets/chevalier.py

Exercice : Ecrivez une autre fonction, nommée chevalier_ret :

  • qui prend 2 paramètres : un entier n et un booléen
  • qui retourne une chaine de caractères de la chaîne nee! ou NEE! en fonction du paramètre booléen, chaîne répétée n fois.

Appelez cette fonction et affichez sa valeur de retour.

Voici quelques exemples montrant comment cette fonction doit se comporter:

a = chevalier_ret(1, True)
print(a)
NEE!
print(chevalier_ret(3, False))
nee!nee!nee!
print(chevalier_ret(6, True))
NEE!NEE!NEE!NEE!NEE!NEE!
In [51]:
# Votre code ici
def chevalier_ret(n, cri):
    pass
In [52]:
# Vérifions que tout fonctionne bien:
a = chevalier_ret(1, True)
print(a)
a = chevalier_ret(3, False)
print(a)
a = chevalier_ret(6, True)
print(a)
None
None
None
In [53]:
# Décommentez puis exécutez pour afficher le corrigé:
#%load exos/snippets/chevalier_ret.py

Utilisation de valeurs par défaut

In [54]:
def somme(x, y=1):
    return x + y

print(somme(1, 2))
3

Si sa valeur n'est pas spécifiée lors de l'appel, le paramètre y prend la valeur par défaut (ici : 1)

In [55]:
print(somme(4))
5

Note : Les arguments ayant une valeur par défaut doivent être placés en dernier.

Utilisation des arguments par leur nom

Si les arguments sont explicitiment nommés lors de l'appel, leur ordre peut être changé :

In [56]:
def diff(x, y):
    return x - y

print(diff(4, 7))
print(diff(y=7, x=4))
-3
-3

Capture d'arguments non définis

Arguments positionnels dans un tuple

On définit une fonction dont l'argument est *args :

In [57]:
def fonc(*args):
    # args est un tuple :
    print(type(args))
    # ses éléments sont les arguments passés lors de l'appel :
    print(args)

On l'appelle avec n'importe quelle séquence d'arguments :

In [58]:
fonc("n'importe", "quel nombre et type de", "paramètres", 5, [1, 'toto'], None)
<class 'tuple'>
("n'importe", 'quel nombre et type de', 'paramètres', 5, [1, 'toto'], None)
Arguments nommés dans un dictionnaire

On définit une fonction dont l'argument est **kwargs :

In [59]:
def fonc(**kwargs):
    # kwargs est un dictionnaire :
    print(type(kwargs))
    # ses éléments sont les arguments nommés passés lors de l'appel
    print(kwargs)

On l'appelle en nommant les arguments :

In [60]:
fonc(x=1, y=2, couleur='rouge', epaisseur=2)
<class 'dict'>
{'x': 1, 'y': 2, 'couleur': 'rouge', 'epaisseur': 2}

On peut combiner ce type d'arguments pour une même fonction :

In [61]:
def fonc(n, *args, **kwargs):  # cet ordre est important
    print("n =", n)
    print("args =", args)
    print("kwargs =", kwargs)

print("appel 1")
fonc(3)
print("appel 2")
fonc(3, 'bacon')
print("appel 3")
fonc(2, 'spam', 'egg', x=1, y=2, couleur='rouge', epaisseur=2)  
appel 1
n = 3
args = ()
kwargs = {}
appel 2
n = 3
args = ('bacon',)
kwargs = {}
appel 3
n = 2
args = ('spam', 'egg')
kwargs = {'x': 1, 'y': 2, 'couleur': 'rouge', 'epaisseur': 2}

Remarques :

  • les noms args et kwargs ne sont que des conventions (à respecter, toutefois !), seul le caractère * est déterminant
  • l'ordre (arg1, arg2, ..., *args, **kwargs) doit être strictement respecté

Packing/unpacking

  • La syntaxe *args dans la définition de la fonction correspond à une opération de packing : Python transforme une séquence de variables en tuple.

  • L'inverse existe : ça s'appelle l'unpacking.

  • Le packing/unpacking se pratique déjà par la manipulation des tuples :

In [62]:
trio = "sax", "drums", "bass"  # packing
print(trio)
you, her, him = trio           # unpacking
print(you)
print(her)
print(him)
('sax', 'drums', 'bass')
sax
drums
bass

Il peut également se pratiquer dans le passage d'arguments de fonction

In [63]:
def fonc(*args):
    print(args)

fonc(you, her, him)  # ici on liste directement les arguments
fonc(*trio)          # là on "unpack" un tuple
('sax', 'drums', 'bass')
('sax', 'drums', 'bass')

Et de la même façon pour un dictionnaire :

In [64]:
def fonc(**kwargs):
    print(kwargs)

trio_dict = {"sax": "you", "drums": "her", "bass": "him"}
fonc(sax="you", drums="her", bass="him")  # ici on liste directement les arguments nommés
fonc(**trio_dict)                         # là on "unpack" un dictionnaire
{'sax': 'you', 'drums': 'her', 'bass': 'him'}
{'sax': 'you', 'drums': 'her', 'bass': 'him'}

Espace de nommage et portée des variables

1er exemple

On veut illustrer le mécanisme de l'espace de nommage des variables :

In [65]:
def func1():
    a = 1
    print("Dans func1(), a =", a)

def func2():
    print("Dans func2(), a =", a)

a = 2
func1()
func2()
print("Dans l'espace englobant, a =", a)
Dans func1(), a = 1
Dans func2(), a = 2
Dans l'espace englobant, a = 2

Cet exemple montre deux comportements :

  1. Une variable définie localement à l'intérieur d'une fonction cache une variable du même nom définie dans l'espace englobant (cas de func1()).
  2. Quand une variable n'est pas définie localement à l'intérieur d'une fonction, Python va chercher sa valeur dans l'espace englobant (cas de func2()).

2ème exemple

On veut illustrer le mécanisme de portée des variables au sein des fonctions :

In [66]:
def func():
    a = 1
    bbb = 2
    print('Dans  func() : a =', a)
    
a = 2
print("Avant func() : a =", a)
func()
print("Après func() : a =", a)
Avant func() : a = 2
Dans  func() : a = 1
Après func() : a = 2

Les variables définies localement à l'intérieur d'une fonction sont détruites à la sortie de cette fonction. Ici, la variable bbb n'existe pas hors de la fonction func(), donc Python renvoie une erreur si on essaye d'utiliser bbb depuis l'espace englobant :

In [67]:
# Cette cellule génère une erreur
print(bbb)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[67], line 2
      1 # Cette cellule génère une erreur
----> 2 print(bbb)

NameError: name 'bbb' is not defined

Fonctions built-in

Ces fonctions sont disponibles dans tous les contextes. La liste complète est détaillée ici. En voici une sélection :

  • dir(obj) : retourne une liste de toutes les méthodes et attributs de l'objet obj
  • dir() : retourne une liste de tous les objets du contexte courant
  • eval(expr) : analyse et exécute la chaîne de caractère expr
In [68]:
a = 1
b = eval('a + 1')
print(f"b est de type {type(b)} et vaut {b}")
b est de type <class 'int'> et vaut 2
  • globals() : retourne un dictionnaire des variables présentes dans le contexte global
  • locals() : idem globals() mais avec le contexte local
  • help(obj) : affiche l’aide au sujet d’un objet
  • help() : affiche l’aide générale (s'appelle depuis l'interpréteur interactif)
  • input(prompt) : retourne une chaîne de caractère lue dans la console après le message prompt
In [69]:
reponse = input('Ca va ? ')  # Seule la variante input() fonctionne dans un notebook

if reponse.lower() in ('o', 'oui', 'yes', 'y', 'ok', 'da', 'jawohl', 'ja'):
    print('Supercalifragilisticexpialidocious')
else:
    print('Faut prendre des vacances...')
---------------------------------------------------------------------------
StdinNotImplementedError                  Traceback (most recent call last)
Cell In[69], line 1
----> 1 reponse = input('Ca va ? ')  # Seule la variante input() fonctionne dans un notebook
      3 if reponse.lower() in ('o', 'oui', 'yes', 'y', 'ok', 'da', 'jawohl', 'ja'):
      4     print('Supercalifragilisticexpialidocious')

File /opt/conda/lib/python3.11/site-packages/ipykernel/kernelbase.py:1201, in Kernel.raw_input(self, prompt)
   1199 if not self._allow_stdin:
   1200     msg = "raw_input was called, but this frontend does not support input requests."
-> 1201     raise StdinNotImplementedError(msg)
   1202 return self._input_request(
   1203     str(prompt),
   1204     self._parent_ident["shell"],
   1205     self.get_parent("shell"),
   1206     password=False,
   1207 )

StdinNotImplementedError: raw_input was called, but this frontend does not support input requests.
  • len(seq) : retourne la longueur de la séquence seq

  • max(seq) : retourne le maximum de la séquence seq

  • min(seq) : retourne le minimum de la séquence seq

  • range([start=0], stop[, step=1]) : retourne un itérateur d'entiers allant de start à stop - 1, par pas de step.

In [70]:
print(list(range(10)))
print(list(range(5, 10, 2)))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[5, 7, 9]
  • repr(obj): affiche la représentation de l'objet obj.
  • reversed(seq) : retourne l’inverse de la séquence seq
  • sorted(seq) : retourne une séquence triée à partir de la séquence seq
  • sum(seq) : retourne la somme des éléments de la séquence seq

Exercices sur les fonctions

Exercice 1

Ecrire une fonction stat() qui prend en argument une liste d'entiers et retourne un tuple contenant :

  • la somme
  • le minimum
  • le maximum

des éléments de la liste

In [71]:
def stat(a_list):
    # votre fonction
    pass
In [72]:
# Décommentez puis exécutez pour afficher le corrigé:
#%load exos/snippets/stat.py

Exercice 2 : écriture d'un wrapper de fonction

Noël arrive vite... Amusez-vous avec les boules de décoration !

sapin

Soit une fonction boule() capable d'accrocher une boule de couleur à la position (x, y) d'un sapin.

Exercice inspiré du Mooc de l'INRIA Python : des fondamentaux à l'utilisation du langage

In [73]:
def boule(x, y, couleur='bleue'):
    print(f"J'accroche une boule en ({x}, {y}) de couleur {couleur}")

# On place la première boule sur le sapin
boule(1, 2)
# Puis une autre, etc.
boule(3, 4, couleur='rouge')
J'accroche une boule en (1, 2) de couleur bleue
J'accroche une boule en (3, 4) de couleur rouge

Ecrire une fonction wrapper boule_or() qui crée des boules dorées en appelant la fonction boule(). Dans le futur, on souhaite modifier la fonction boule() pour lui faire accepter un nouvel argument rendu (brillant, mat, etc.). La fonction boule_or() devra continuer à fonctionner après cette modification de la fonction boule() et intégrer la nouvelle fonctionnalité rendu sans qu'il soit nécessaire de modifier boule_or().

In [74]:
# Cellule à modifier
def boule_or(*args, **kwargs):
    # Votre code ici
    pass

# On place une boule en or sur le sapin
boule_or(1, 2)

Maintenant, on met à jour la fonction boule() :

In [75]:
def boule(x, y, couleur='bleue', rendu='mat'):
    print(f"J'accroche une boule en ({x}, {y}) de couleur {couleur} et de rendu {rendu}.")
boule(1, 3, couleur='jaune', rendu='brillant')
J'accroche une boule en (1, 3) de couleur jaune et de rendu brillant.

Vérifier que votre fonction boule_or() marche encore et gère la nouvelle fonctionnalité :

In [76]:
boule_or(3, 1, rendu='brillant') # doit retourner :
# J'accroche une boule en (3, 1) de couleur or et de rendu brillant.
In [77]:
# Décommentez puis exécutez pour afficher le corrigé:
#%load exos/snippets/boule.py

Exceptions

Pour signaler des conditions particulières (erreurs, évenements exceptionnels), Python utilise un mécanisme de levée d'exceptions.

In [78]:
# Cette cellule génère une erreur
raise Exception
---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
Cell In[78], line 2
      1 # Cette cellule génère une erreur
----> 2 raise Exception

Exception: 

Ces exceptions peuvent embarquer des données permettant d'identifier l'évenement producteur.

In [79]:
# Cette cellule génère une erreur
raise Exception('Y a une erreur')
---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
Cell In[79], line 2
      1 # Cette cellule génère une erreur
----> 2 raise Exception('Y a une erreur')

Exception: Y a une erreur

La levée d'une exception interrompt le cours normal de l'exécution du code et "remonte" jusqu'à l'endroit le plus proche gérant cette exception.

Pour intercepter les exceptions, on écrit :

try:
    <bloc de code 1>
except Exception:
    <bloc de code 2>
In [80]:
try:
    print('ici ca fonctionne')
    # ici on détecte une condition exceptionnelle, on signale une exception
    raise Exception('y a un bug')
    print("on n'arrive jamais ici")
except Exception as e:
    # L'exécution continue ici
    print(f"ici on peut essayer de corriger le problème lié à l'exception : Exception({e})")
print("et après, ça continue ici")
ici ca fonctionne
ici on peut essayer de corriger le problème lié à l'exception : Exception(y a un bug)
et après, ça continue ici

Exemple illustrant le mécanisme de remontée des exceptions d'un bloc à l'autre :

In [81]:
def a():
    raise Exception('coucou de a()')

def b():
    print('b() commence')
    a()
    print('b() finit')

try:
    b()
except Exception as e:
    print("l'exception vous envoie le message :", e)
b() commence
l'exception vous envoie le message : coucou de a()

Exercice

Ecrivez une fonction openfile() :

  • qui demande à l'utilisateur un nom de fichier à ouvrir
  • qui gère correctement les fichiers inexistants.
  • qui affiche la première ligne du fichier ouvert
  • qui retourne une valeur booléenne indiquant que le fichier a été ouvert ou non.
In [82]:
# Votre code ici
def openfile():
    pass

print(openfile())
None
In [83]:
# Décommentez puis exécutez pour afficher le corrigé:
#%load exos/snippets/openfile.py

Pour plus d'informations sur les exceptions, lire ce tutoriel.

Les gestionnaires de contexte

Pour faciliter la gestion des obligations liées à la libération de ressources, la fermeture de fichiers, etc... Python propose des gestionnaires de contexte introduits par le mot-clé with.

In [84]:
with open('exos/interessant.txt', 'r') as fichier_ouvert:
    # Dans ce bloc de code le fichier est ouvert en lecture, on peut l'utiliser normalement
    print(fichier_ouvert.read())
# Ici, on est sorti du bloc et du contexte : le fichier à été fermé automatiquement
Si vous lisez ce texte alors
...
vous savez lire un fichier avec Python !

In [85]:
# Cette cellule génère une erreur
print(fichier_ouvert.read())
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[85], line 2
      1 # Cette cellule génère une erreur
----> 2 print(fichier_ouvert.read())

ValueError: I/O operation on closed file.

Exercice : Reprenez le code de l'exercice précédent, et utilisez with pour ne pas avoir à utiliser la méthode close().

In [86]:
# Votre code ici
In [87]:
# Décommentez puis exécutez pour afficher le corrigé:
#%load exos/snippets/openfile2.py

Il est possible de créer de nouveaux gestionnaires de contexte, pour que vos objets puissent être utilisés avec with et que les ressources associées soient correctement libérées.

Pour plus d'informations sur la création de gestionnaires de contexte, voir la documentation.

Les compréhensions de listes

Python a introduit une facilité d'écriture pour les listes qui permet de rendre le code plus lisible car plus concis.

On construit par exemple la liste [0, 1, 2, ..., 9] :

In [88]:
Liste1 = list(range(10))
print(Liste1)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

On veut maintenant une liste ne contenant que les éléments pairs de la liste Liste1.

In [89]:
ListePaire = []
for i in Liste1: 
    if (i % 2) == 0:
        ListePaire.append(i)
print(ListePaire)
[0, 2, 4, 6, 8]

À présent, on fait la même chose en compréhension de liste

In [90]:
ListePaire = [i for i in Liste1 if (i % 2) == 0]
print(ListePaire)
[0, 2, 4, 6, 8]

Cette concision peut être utile, mais n'en abusez pas, si vous commencez à avoir une compréhension de liste trop complexe à écrire en une simple ligne, utilisez plutôt des boucles et conditions explicites.
Plus d'informations dans ce tutoriel.

Les expressions génératrices

C'est une forme d'écriture, très proche des compréhensions de listes, mais qui ne crée pas de nouvel objet liste immédiatement. Les items sont produits à la demande, plus loin dans le code, là où ils sont nécessaires, ce qui économise du temps et de la mémoire.

In [91]:
generateur_pairs = (i for i in Liste1 if (i % 2) == 0)

Le générateur ne contient pas de données à proprement parler :

In [92]:
print(generateur_pairs)
<generator object <genexpr> at 0x7fc2d747e4d0>

Pour visualiser son comportement, on peut l'utiliser pour créer une liste :

In [93]:
print(list(generateur_pairs))
[0, 2, 4, 6, 8]

Plus d'informations dans ce tutoriel.

Modules

  • Python fournit un système de modularisation du code qui permet d'organiser un projet contenant de grandes quantités de code et de réutiliser et de partager ce code entre plusieurs applications.

  • L'instruction import permet d'accéder à du code situé dans d'autres fichiers. Cela inclut les nombreux modules de la bibliothèque standard, tout comme vos propres fichiers contenant du code.

  • Les objets (variables, fonctions, classes, etc.) du module sont accessibles de la manière suivante :

module.variable
module.fonction(parametre1, parametre2, ...)

Ou plus généralement :

module.attribut
In [94]:
# Pour utiliser les fonctions mathématiques du module 'math'
import math

print(f'{math.pi:.2f}')
print(f'{math.sin(math.pi):.2f}')
3.14
0.00

Créer ses propres modules

  • Il suffit de placer votre code dans un fichier avec l'extension .py
  • Le module stocké dans le fichier mon_module.py s'importe avec la syntaxe :
import mon_module

Cas 1 : le fichier module est directement importable

  • Cas 1.a : le module est déjà installé dans l'environnement d'exécution

    • soit il fait partie de la bibliothèque standard
    • soit il a été installé (avec conda, avec pip, etc.)
  • Cas 1.b : le fichier module se trouve :

    • dans le même répertoire que le script qui l'importe
    • dans un répertoire référencé par la variable d'environnement PYTHONPATH

Le fichier exos/mon_module.py contient du code définissant ma_variable et ma_fonction(). On copie ce fichier dans le répertoire courant.

In [95]:
import shutil  # shutil fait partie de la bibliothèque standard
shutil.copy("exos/mon_module.py", ".")  # On copie le fichier dans le répertoire d'exécution du notebook
%pycat mon_module.py

On peut maintenant importer mon_module depuis le notebook :

In [96]:
import mon_module  # Notez la syntaxe: nom_du_fichier sans l'extension.
print(mon_module.ma_variable)
mon_module.ma_fonction()  # On accède ainsi à l'attribut ma_fonction() du module mon_module
27
un appel à ma_fonction()

On peut importer un module sous un autre nom (pour le raccourcir, en général) :

In [97]:
import mon_module as mm
mm.ma_fonction()
un appel à ma_fonction()

On peut importer un objet particulier d'un module :

In [98]:
from mon_module import ma_fonction
ma_fonction()
un appel à ma_fonction()

Ou encore en définissant un alias avec as

In [99]:
from mon_module import ma_fonction as ma_fonc
ma_fonc()
un appel à ma_fonction()

Note :

Dans le code :

import mon_module
[...]
import mon_module

Le deuxième import n'a pas d'effet car le module n'est importé qu'une seule fois au sein d'une même instance Python. Toutefois, il existe des moyens de le réimporter de force avec la bibliothèque importlib.

Exercice : Modifiez le code contenu dans le fichier mon_module.py, et reexécutez la cellule ci-dessus. Que remarquez-vous ?

Cas 2 : le fichier module se trouve ailleurs

On peut distinguer deux cas d'usage :

  • Cas 2.a : on veut aller chercher du code dans un autre projet python qui n'est pas installé dans l'environnement courant
  • Cas 2.b : on travaille sur un gros projet structuré en modules stockés dans une arborescence de sous-répertoires.
Cas 2.a : exemple avec le fichier exos/ext_dir/module_externe.py

Le fichier module_externe.py contient :

In [100]:
%pycat exos/ext_dir/module_externe.py

On ajoute le répertoire exos/ext_dir à la liste des répertoires scannés par Python :

In [101]:
import sys
sys.path.append("./exos/ext_dir")
print(sys.path)
['/builds/urfist/cours-python/notebooks', '/opt/conda/lib/python311.zip', '/opt/conda/lib/python3.11', '/opt/conda/lib/python3.11/lib-dynload', '', '/home/jovyan/.local/lib/python3.11/site-packages', '/opt/conda/lib/python3.11/site-packages', './exos/ext_dir']

Le module s'importe alors directement avec :

In [102]:
import module_externe
Je suis dans le module module_externe
Cas 2.b : un projet structuré en sous-répertoires

Dans ce cas, on parle de paquets (packages) et de sous-paquets :

  • n'importe quel répertoire contenant un fichier __init__.py est un paquet python
  • chaque sous-répertoire est un sous-paquet du répertoire (ou paquet) parent
  • une arborescence de paquets permet d'organiser le code de manière hiérarchique.
  • On accède aux sous-paquets avec la notation :
import paquet.sous_paquet.sous_sous_paquet...
Exemple avec le répertoire exos :
In [103]:
#!tree  exos -P "*.py" -I __pycache__
exos
[...]
├── sous_paquet
│   ├── __init__.py
│   ├── module_a.py
│   └── module_b.py
├── sous_paquet2
│   ├── __init__.py
│   └── module_c.py
[...]

Dans ce cas, le module exos/sous_paquet/module_a.py contenant :

In [104]:
%pycat exos/sous_paquet/module_a.py

s'importe de la façon suivante :

In [105]:
import exos.sous_paquet.module_a
# On appelle fonction()
exos.sous_paquet.module_a.fonction() 
Je suis dans le module exos.sous_paquet.module_a
Je suis dans fonction() du module exos.sous_paquet.module_a

ou encore :

In [106]:
from exos.sous_paquet import module_a
# On appelle fonction()
module_a.fonction()
Je suis dans fonction() du module exos.sous_paquet.module_a

Exercice : Importer directement la fonction fonction de exos/sous_paquet/module_b.py sous le nom func et appelez-là.

In [107]:
%pycat exos/sous_paquet/module_b.py
In [108]:
# Votre code ici
# func()
In [109]:
# Décommentez puis exécutez pour afficher le corrigé:
#%load exos/snippets/import_module.py

Remarque

Si __init__.py existe, le code qu'il contient est exécuté lors de l'import du paquet.

In [110]:
%pycat exos/sous_paquet/__init__.py

Dans ce cas, on peut importer directement module_a et module_b :

In [111]:
from exos.sous_paquet import *
module_b.fonction()
Je suis dans le module exos.sous_paquet.module_b
Je suis dans fonction() du module exos.sous_paquet.module_b

Imports relatifs

Depuis le module

exos/sous_paquet2/module_c.py

on peut importer le module

exos/sous_paquet/module_b.py

en utilisant la syntaxe :

from .. import sous_paquet
from ..sous_paquet import module_b
In [112]:
%pycat exos/sous_paquet2/module_c.py

On importe module_c et on appelle ses attributs :

In [113]:
from exos.sous_paquet2 import module_c
module_c.fonction()
module_c.module_b.fonction()
Je suis dans le module exos.sous_paquet2.module_c
Je suis dans fonction() du module exos.sous_paquet2.module_c
Je suis dans fonction() du module exos.sous_paquet.module_b

Pour plus d'informations sur les modules et paquets, voir ce tutoriel.

Exercice

  1. dans le répertoire exos/sous_paquet2/ copiez la définition de la fonction boule() (cf. exercice ci-dessus) dans un fichier nommé deco.py
  2. dans le répertoire exos/sous_paquet/ éditez un fichier nommé noel.py contenant :
  • l'import de la fonction boule() depuis exos/sous_paquet2/deco.py
  • la définition de la fonction boule_or() (cf. corrigé de l'exercice)
def boule_or(*args, **kwargs):
    # seule instruction qui fait une hypothèse
    # sur la signature de la fonction boule() :
    kwargs['couleur'] = "or"
    return boule(*args, **kwargs)
  1. Décommentez la cellule ci-dessous, et exécutez-là pour vérifier que la fonction boule_or() est bien importée.
In [114]:
# from exos.sous_paquet.noel import boule_or
# boule_or(1, 2)
In [115]:
# Solution : décommentez les lignes suivantes pour positionner le corrigé

#!cp exos/sous_paquet/deco.py exos/sous_paquet2/
#!cp exos/sous_paquet2/noel.py exos/sous_paquet/

from exos.sous_paquet.noel import boule_or
boule_or(1, 2)
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
Cell In[115], line 6
      1 # Solution : décommentez les lignes suivantes pour positionner le corrigé
      2 
      3 #!cp exos/sous_paquet/deco.py exos/sous_paquet2/
      4 #!cp exos/sous_paquet2/noel.py exos/sous_paquet/
----> 6 from exos.sous_paquet.noel import boule_or
      7 boule_or(1, 2)

ModuleNotFoundError: No module named 'exos.sous_paquet.noel'

Quelques modules de la stdlib

La bibliothèque standard de Python est incluse dans toute distribution de Python. Elle contient en particulier une panoplie de modules à la disposition du développeur.

string

  • find()
  • count()
  • split()
  • join()
  • strip()
  • upper()
  • replace()

math

  • log()
  • sqrt()
  • cos()
  • pi
  • e

os

  • listdir()
  • getcwd()
  • getenv()
  • chdir()
  • environ()
  • os.path : exists(), getsize(), isdir(), join()

sys

  • argv
  • exit()
  • path

Mais bien plus sur la doc officielle de la stdlib !

Bonnes pratiques

Commentez votre code

  • pour le rendre plus lisible
  • pour préciser l'utilité des fonctions, méthodes, classes, modules, etc...
  • pour expliquer les parties complexes

Documentez en utilisant les docstrings

  • Juste après la signature de la fonction, on utilise une chaîne de caractère appelée docstring délimitée par """ """
  • la docstring permet de documenter la fonction au plus près de sa définition
  • cette docstring est affichée par help(fonction)
  • la docstring est utilisée par les outils de documentation automatique comme Sphinx.
In [116]:
def nrien(n):
    """Retourne n fois 'rien'"""
    return 'rien' * n

help(nrien)
# ou dans ipython:
nrien?
Help on function nrien in module __main__:

nrien(n)
    Retourne n fois 'rien'

Plus d'information sur les Docstrings dans la documentation officielle et l'extension Napoleon pour Sphinx.

Utilisez les annotations de type

Dans la version 3.5, Python a introduit le mécanisme d'annotation de type :

In [117]:
def nrien(n: int) -> str:
    """Retourne n fois 'rien'"""
    return 'rien' * n

nrien(3)
Out[117]:
'rienrienrien'

Ces annotations ne modifient pas l'exécution du code, mais elles présentent des avantages :

  • elles indiquent au lecteur le type des arguments et de valeurs de retour,
  • elles sont utilisées par Sphinx et par les vérificateurs de code statique comme mypy.

Cela permet d'éviter de nombreux bugs !

Conventions d'écriture

Habituez-vous assez tôt aux conventions préconisées dans la communauté des utilisateurs de python. Cela vous aidera a relire plus facilement le code écrit par d'autres, et aidera les autres (et vous-même !) à relire votre propre code.

Ces conventions sont décrites dans le document PEP n°8 (Python Enhancement Proposal). Les outils comme pep8 ou ruff) permettent de formater automatiquement le code source pour respecter ces règles.

Exercice :

  1. Lisez le PEP8, et corrigez toutes les fautes commises dans ce notebook
  2. Envoyez le résultat à votre enseignant

Organisez votre code source

Pour la lisibilité, la réutilisation et la maintenance, le principe à retenir est d'évitez le copier-coller de code :

  • placez dans une même fonction les portions de code exécutées plusieurs fois
  • placez dans un même module les variables, fonctions et classes partagées entre plusieurs parties ou programmes
  • dans les projets importants, regrouper vos modules en packages

Utiliser les environnements de développement intégrés :

Snapshot Spyder

Utilisez un gestionnaires de versions

  • git : gestion distribuée, largement majoritaire aujourd'hui
  • subversion : gestion centralisée, outil plus ancien

Faire des enregistrements (commits) fréquents et cohérents.

Héberger vos dépôts de sources

  • github : des millions d'utilisateurs et de dépôts
  • gitlab : un concurrent moins visibles mais aux fonctionnalités très intéressantes

Vérificateurs de code source

  • autopep8, ruff ou black : pour normaliser la mise en page
  • pylint ou ruff : pour vérifier que la syntaxe est correcte
  • mypy : pour la vérification de type

Ces modules sont généralement disponibles dans les IDE avancées.

Tests en Python

En programmation, on utilise des tests de non régression pour vérifier que les nouveaux développements et les corrections de bugs n'entraînent pas de pertes de fonctionalités et de nouveaux bugs. On distingue généralement :

  • les tests unitaires (comportement de fonctions prises séparément)
  • les tests d'intégration (interaction entre plusieurs parties de programme)
  • les tests du système complet

Python dispose de nombreux modules dédiés aux deux premières catégories. Quelques exemples :

  • unittest : fait partie de la bibliothèque standard
  • doctest : le test est basé sur des sorties de sessions interactives stockées généralement dans la docstring de la fonction testée (alourdit le code...)
  • nose : une extension de unittest
  • pytest : syntaxe simple et nombreuses fonctionnalités

Une synthèse des outils existants sur cette page.

Environnements virtuels

Les environnements virtuels sont très utiles pour le développement car ils permettent d'isoler les installations et les exécutions.

Philosophie du langage : le zen de Python

PEP 20

In [118]:
import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

Python 3.x vs 2.x

C'est le futur, et incidemment aussi le présent voire même le passé...

  • Quoi : une version qui casse la compatibilité ascendante
  • Pourquoi : nettoyage de parties bancales du langage accumulées au fil du temps
  • Python 3.0 est sorti en 2008
  • Python 2.7 est sorti en 2010 : EOL, fin de vie, (mal-)heureusement longue à venir
  • Un certain nombre de choses n'a pas encore été converti pour fonctionner avec
  • Les distributions linux majeures proposent encore la 2.X par défaut, mais la 3 est disponible en parallèle
  • Une partie, la moins disruptive, a quand même été portée vers 2.6 et 2.7 pour aider à la transition
  • Les tutoriels, et autres documentations disponibles sur internet ne sont pas forcément migrées
  • Pour un nouveau projet, recherchez un peu avant de vous lancer, pour vérifier les besoins en librairies externes
  • Les implémentations tierces d'interpréteurs python peuvent avoir des degrés variables de compatibilité avec les versions 3.x
  • Les modules comportant des extentions en C sont plus compliqués à porter.

Différences notables entre Python 2 et Python 3

  • Division entière
  • print()
  • variable à durée de vie plus stricte (boucles, etc...)
  • toutes les classes sont du nouveau type
  • Les chaînes de caractères sont en UTF-8 par défaut & encoding(s) & byte() interface
  • stdlib changée
  • range() vs xrange()
  • outils 2to3.py, 3to2, python-modernize, futurize
  • pylint --py3k
  • module de compatibilité : six

Plus d'informations sur le wiki officiel.

Exercice de synthèse : décodage de l'ARN

Enoncé

On souhaite traduire une séquence d'ARN stockée dans le fichier exos/arn.txt en une séquence d'acides aminés. Pour ce faire, on dispose du code génétique (simplifié) stocké dans exos/code_genetique.txt (tableau tiré de wikipedia).

Le fichier code_genetique.txt contient pour chaque ligne :

Nom de l'acide aminé; lettre unique; nom abrégé; codon1, codon2, etc.
In [119]:
%pycat exos/code_genetique.txt

Consignes

  1. Ecrire le code qui :

    • ouvre le fichier code génétique
    • lit son contenu pour générer le dictionnaire suivant :
code = {letter: {name: ...,
                 abrv: ...,
                 codons: [..., ..., ]}}
In [120]:
# votre code ici 
In [121]:
# Décommentez puis exécutez pour afficher le corrigé :
#%load exos/snippets/arn_1.py
  1. Construire le dictionnaire inverse icode = {codon: letter} qui traduit un codon en acide aminé représenté par sa lettre symbole.
In [122]:
# votre code ici
In [123]:
# Décommentez puis exécutez pour afficher le corrigé :
#%load exos/snippets/arn_2.py
  1. Ecrire la fonction decode() qui admet comme argument la chaîne de caractères représentant une séquence d'ARN et retourne la séquence d'acides aminés (appelée peptide) correspondante sous forme de chaîne de caractères. En fonction de l'argument optionnel form, cette fonction retournera :
    • soit une séquence de symboles (lettres)
    • soit des abréviations d'acides aminés (séparées par des -).
In [124]:
# votre code ici
In [125]:
# Décommentez puis exécutez pour afficher le corrigé :
#%load exos/snippets/arn_3.py
  1. Appliquer la fonction decode() à la chaîne de caractères représentant l'ARN contenue dans exos/arn.txt.
In [126]:
# votre code ici
In [127]:
# Décommentez puis exécutez pour afficher le ccorrigé :
#%load exos/snippets/arn_4.py

Solution complète

In [128]:
# Décommentez puis exécutez pour afficher le ccorrigé :
#%load exos/snippets/arn.py

Exercices complémentaires

Chaines de caractères

Ecrivez les fonctions suivantes, sans utiliser upper() ni lower() :

  • majuscule('azERtyUI') $\rightarrow$ 'AZERTYUI'
  • minuscule('azERtyUI') $\rightarrow$ 'azertyui'
  • inverse_casse('azERtyUI') $\rightarrow$ 'AZerTYui'
  • nom_propre('azERtyUI') $\rightarrow$ 'Azertyui'

Indice : cet exercice s'apparente à de la "traduction"...

In [129]:
def majuscule(chaine):
    # Votre code ici
    pass

def minuscule(chaine):
    # Votre code ici
    pass

def inverse_casse(chaine):
    # Votre code ici
    pass

def nom_propre(chaine):
    # Votre code ici
    pass
    
assert majuscule('azERtyUI') == 'AZERTYUI'
assert minuscule('azERtyUI') == 'azertyui'
assert inverse_casse('azERtyUI') == 'AZerTYui'
assert nom_propre('azERtyUI') == 'Azertyui'
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Cell In[129], line 17
     13 def nom_propre(chaine):
     14     # Votre code ici
     15     pass
---> 17 assert majuscule('azERtyUI') == 'AZERTYUI'
     18 assert minuscule('azERtyUI') == 'azertyui'
     19 assert inverse_casse('azERtyUI') == 'AZerTYui'

AssertionError: 
In [130]:
# Décommentez puis exécutez pour afficher le ccorrigé :
#%load exos/snippets/casse.py

Récursion

Les fonctions dites récursives sont des fonctions qui font appel à elles-même, en résolvant une partie plus petite du problème à chaque appel, jusqu'à avoir un cas trivial à résoudre.

Par exemple, pour calculer la somme de tous les nombres de 0 jusqu'à x, on peut utiliser une fonction récursive. Par exemple, pour $x = 10$, on a :

  • $\sum_{n=0}^{10} n = 10 + \sum_{n=0}^{9} n$

  • $\sum_{n=0}^{9} n = 9 + \sum_{n=0}^{8} n$

  • etc.

In [131]:
def sum_to(x):
    if x == 0:
        return 0
    return x + sum_to(x - 1)
In [132]:
print(sum_to(9))
45

La fonction mathématique factorielle est similaire, mais calcule le produit de tous les nombres de 1 jusqu'à x.

In [133]:
def fact(x):
    if x == 1:
        return 1
    return x * fact(x - 1)
In [134]:
print(fact(5), fact(9))
120 362880

La fonction mathématique qui calcule la suite des nombres de Fibonacci, peut être décrite de la façon suivante :

  • fibo(0) = 0
  • fibo(1) = 1

Et pour toutes les autres valeurs :

  • fibo(x) = fibo(x - 1) + fibo(x - 2)

Exercice : écrivez une fonction récursive fibo(x) qui renvoie le x-ième nombre de la suite de Fibonnaci.

In [135]:
def fibo(x):
    # Votre code ici
    pass
In [136]:
print(fibo(9))
None
In [137]:
# Décommentez puis exécutez pour afficher le ccorrigé :
#%load exos/snippets/fibo.py