Aller au contenu

21 Remarques complémentaires

21.1 Différences Python 2 et Python 3

Python 3 est la version de Python qu'il faut utiliser.

Néanmoins, Python 2 a été employé pendant de nombreuses années par la communauté scientifique et vous serez certainement confrontés à un programme écrit en Python 2. Voici quelques éléments pour vous en sortir :

21.1.1 Le mot-clé print / la fonction print()

En Python 2 print est un mot-clé du langage (en anglais statement) au même titre que for, if, def, etc. Il s'utilise ainsi sans parenthèse. Par exemple :

>>> print 12
12
>>> print "girafe"
girafe

Par contre en Python 3, print() est une fonction. Ainsi, si vous n'utilisez pas de parenthèse, Python vous renverra une erreur :

>>> print 12
  File "<stdin>", line 1
    print 12
           ^
SyntaxError: Missing parentheses in call to 'print'

21.1.2 Division d'entiers

En Python 3, la division de deux entiers, se fait naturellement, c'est-à-dire que l'opérateur / renvoie systématiquement un float. Par exemple :

>>> 3 / 4
0.75
Il est également possible de réaliser une division entière avec l'opérateur // :
>>> 3 // 4
0

La division entière renvoie finalement la partie entière du nombre 0.75, c'est-à-dire 0.

Attention ! En Python 2, la division de deux entiers avec l'opérateur / correspond à la division entière, c'est-à-dire le résultat arrondi à l'entier inférieur. Par exemple :

>>> 3 / 5
0
>>> 4 / 3
1

Faites très attention à cet aspect si vous programmez encore en Python 2, c'est une source d'erreur récurrente.

21.1.3 La fonction range()

En Python 3, la fonction range() renvoie un objet de type range (cf. chapitres 5 Boucles et comparaisons et 13 Containers, dictionnaires, tuples et sets) :

>>> range(3)
range(0, 3)

Comme on a vu au chapitre 5 Boucles et comparaisons, ces objets sont itérables produisant successivement les nombres 0, puis 1 puis 2 sur notre exemple :

>>> for i in range(3):
...     print(i)
...
0
1
2

En Python 2, la fonction range() renvoie une liste. Par exemple :

>>> range(3)
[0, 1, 2]
>>> range(2, 6)
[2, 3, 4, 5]

La création de liste avec range() était pratique mais très peu efficace en mémoire lorsque l'argument donné à range() était un grand nombre.

D'ailleurs la fonction xrange() est disponible en Python 2 pour faire la même chose que la fonction range() en Python 3. Attention, ne vous mélangez pas les pinceaux !

>>> range(3)
[0, 1, 2]
>>> xrange(3)
xrange(3)

Remarque

Pour générer une liste d'entiers avec la fonction range() en Python 3, vous avez vu dans le chapitre 4 Listes qu'il suffit de l'associer avec la fonction list(). Par exemple :

>>> list(range(10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Remarque

En Python 3, la fonction range() est ce qu'on appelle un générateur dans le sens où elle génère un objet contenant une série de valeurs, utilisables une à la fois, par itération dans une boucle for. L'objet de type range renvoyé par la fonction est quant à lui appelé itérateur. Si vous souhaitez en savoir un peu plus sur la différence entre un générateur et un itérateur, vous pouvez consulter cette ressource.

21.1.4 Encodage et utf-8

En Python 3, vous pouvez utiliser des caractères accentués dans les commentaires ou dans les chaîne de caractères.

Ce n'est pas le cas en Python 2. Si un caractère accentué est présent dans votre code, cela occasionnera une erreur de ce type lors de l'exécution de votre script :

SyntaxError: Non-ASCII character '\xc2' in file xxx on line yyy, but no encoding
declared; see http://python.org/dev/peps/pep-0263/ for details

Pour éviter ce genre de désagrément, ajoutez la ligne suivante en tout début de votre script :

# coding: utf-8

Si vous utilisez un shebang (voir rubrique précédente), il faudra mettre la ligne # coding: utf-8 sur la deuxième ligne (la position est importante) de votre script :

#! /usr/bin/env python
# coding: utf-8

Remarque

L'encodage utf-8 peut aussi être déclaré de cette manière :

# -*- coding: utf-8 -*-

mais c'est un peu plus long à écrire.

21.2 Gestion des exceptions

Les langages de programmation comme Python contiennent un système de gestion des exceptions. Qu'est-ce qu'une exception ? Sur la page anglaise de Wikipedia, une exception est définie comme une anomalie de l'exécution d'un programme requérant une action spéciale, en général l'arrêt de l'exécution. Le plus souvent, une exception correspond à une erreur que Python rencontre lorsqu'il tente d'exécuter les lignes de code qu'on lui soumet. Par exemple, un problème de syntaxe, une variable ou objet qui prend une valeur aberrante (par exemple diviser par 0, parcourir une liste au delà du nombre d'éléments, etc.).

Le système de gestion des exceptions évite que votre programme « plante » en prévoyant vous-même les sources d'erreurs éventuelles.

Voici un exemple dans lequel on demande à l'utilisateur d'entrer un nombre entier, puis on affiche ce nombre.

>>> nb = int(input("Entrez un nombre entier : "))
Entrez un nombre entier : 23
>>> print(nb)
23

La fonction input() demande à l'utilisateur de saisir une chaîne de caractères. Cette chaîne de caractères est ensuite transformée en nombre entier avec la fonction int().

Si l'utilisateur ne rentre pas un nombre, voici ce qui se passe :

>>> nb = int(input("Entrez un nombre entier : "))
Entrez un nombre entier : ATCG
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: 'ATCG'

L'erreur provient de la fonction int() qui n'a pas pu convertir la chaîne de caractères "ATCG" en nombre entier, ce qui est parfaitement normal. En termes plus techniques, on dira que « Python a levé une exception de type ValueError ». Eh oui il y a de nombreux types d'exceptions différents (voir plus bas) ! Le nom de l'exception apparaît toujours comme le premier mot de la dernière ligne du message d'erreur. Si nous lancions ces lignes de code sous forme de script (du style python script.py), cet exemple conduirait à l'arrêt de l'exécution du programme.

Le jeu d'instructions try / except permet de tester l'exécution d'une commande et d'intervenir en cas de levée d'exception.

>>> try:
...     nb = int(input("Entrez un nombre entier : "))
... except:
...     print("Vous n'avez pas entré un nombre entier !")
...
Entrez un nombre entier : ATCG
Vous n'avez pas entré un nombre entier !

Dans cette exemple, l'exception levée par la fonction int() (qui ne peut pas convertir "ATCG" en nombre entier) est interceptée et déclenche l'affichage du message d'avertissement.

On peut ainsi redemander sans cesse un nombre entier à l'utilisateur, jusqu'à ce que celui-ci en rentre bien un.

>>> while True:
...     try:
...             nb = int(input("Entrez un nombre entier : "))
...             print("Le nombre est", nb)
...             break
...     except:
...             print("Vous n'avez pas entré un nombre entier !")
...             print("Essayez encore")
...
Entrez un nombre entier : ATCG
Vous n'avez pas entré un nombre entier !
Essayez encore
Entrez un nombre entier : toto
Vous n'avez pas entré un nombre entier !
Essayez encore
Entrez un nombre entier : 3.2
Vous n'avez pas entré un nombre entier !
Essayez encore
Entrez un nombre entier : 55
Le nombre est 55

Notez que dans cet exemple, l'instruction while True est une boucle infinie car la condition True est toujours vérifiée. L'arrêt de cette boucle est alors forcé par la commande break lorsque l'utilisateur a effectivement entré un nombre entier.

La gestion des exceptions est très utile dès lors que des données extérieures entrent dans un programme Python, que ce soit directement par l'utilisateur (avec la fonction input()) ou par des fichiers. Cela est fondamental si vous distribuez votre code à la communauté : si les utilisateurs ne connaissent pas Python, un message comme Vous n'avez pas entré un nombre entier ! reste plus clair que ValueError: invalid literal for int() with base 10: 'ATCG'.

Vous pouvez par exemple vérifier qu'un fichier a bien été ouvert.

>>> nom = "toto.pdb"
>>> try:
...     with open(nom, "r") as fichier:
...         for ligne in fichier:
...             print(ligne)
... except:
...     print("Impossible d'ouvrir le fichier", nom)

Si une erreur est déclenchée, c'est sans doute que le fichier n'existe pas à l'emplacement indiqué sur le disque ou que vous n'avez pas les droits pour le lire.

Il est également possible de spécifier le type d'erreur à gérer. Le premier exemple que nous avons étudié peut s'écrire :

>>> try:
...     nb = int(input("Entrez un nombre entier : "))
... except ValueError:
...     print("Vous n'avez pas entré un nombre entier !")
...
Entrez un nombre entier : ATCG
Vous n'avez pas entré un nombre entier !

Ici, on intercepte une exception de type ValueError, ce qui correspond bien à un problème de conversion avec int().

Attention, si vous précisez le type d'exception comme ValueError, le except ValueError n'empêchera pas la levée d'une autre exception.

>>> try:
...     nb = int(variable)
... except ValueError:
...     print("Vous n'avez pas entré un nombre entier !")
...
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
NameError: name 'variable' is not defined

Ici l'exception levée est de type NameError car variable n'existe pas. Alors que si vous mettez except tout court, cela intercepte n'importe quelle exception.

>>> try:
...    nb = int(variable)
... except:
...    print("Vous n'avez pas entré un nombre entier !")
...
Vous n'avez pas entré un nombre entier !
>>>

Vous voyez qu'ici cela pose un nouveau problème : le message d'erreur ne correspond pas à l'exception levée !

Conseils

  • Nous vous conseillons vivement de toujours préciser le type d'exception dans vos except. Cela évite d'intercepter une exception que vous n'aviez pas prévue. Il est possible d'intercepter plusieurs types d'exceptions en passant un tuple à except, par exemple : except (Exception1, Exception2).
  • Pa ailleurs, ne mettez pas trop de lignes dans le bloc du try. Dans un tel cas, il peut être très pénible de trouver la ligne qui a conduit à l'exécution du except. Pire encore, il se peut que des lignes que vous aviez prévues ne soient pas exécutées ! Donc gardez des choses simples dans un premier temps, comme par exemple tester les conversions de type ou vérifier qu'un fichier existe bien et que vous pouvez l'ouvrir.

Il existe de nombreux types d'exception comme RuntimeError, TypeError, NameError, IOError, etc. Vous pouvez aller voir la liste complète sur le site de Python. Nous avions déjà croisé des noms d'exception au chapitre 19 (Avoir la classe avec les objets) en regardant ce que contient le module builtins.

>>> import builtins
>>> dir(builtins)
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException',
[...]
'UserWarning', 'ValueError', 'Warning', 'ZeroDivisionError'
[...]

Leur présence dans le module builtins signifie qu'elles font partie du langage lui même, au même titre que les fonctions de base comme range(), list(), etc.

Avez-vous aussi remarqué que leur nom commence toujours par une majuscule et qu'il peut en contenir plusieurs à la façon CamelCase ? Si vous avez bien lu le chapitre 15 Bonnes Pratiques, avez-vous deviné pourquoi ? Et bien, c'est parce que les exceptions sont des classes. C'est très intéressant car il est ainsi possible d'utiliser l'héritage pour créer ses propres exceptions à partir d'exceptions pré-existantes. Nous ne développerons pas cet aspect, mais en guise d'illustration, regardez ce que renvoit un help() de l'exception OverflowError.

>>> help(OverflowError)
[...]
class OverflowError(ArithmeticError)
 |  Result too large to be represented.
 |
 |  Method resolution order:
 |      OverflowError
 |      ArithmeticError
 |      Exception
 |      BaseException
 |      object

L'exception OverflowError hérite de ArithmeticError, c'est-à-dire qu'OverflowError a été conçue à partir de ArithmeticError et en hérite de tous ses attributs.

Un autre aspect très important que nous avons croisé au chapitre 19 est la possibilité de lever vous-même une exception avec le mot-clé raise. Nous avions vu le code suivant :

if valeur < 0:
    raise ValueError("Z'avez déjà vu une masse négative ? C'est nawak")

La ligne 2 lève une exception ValueError lorsque la variable valeur est négative. L'instruction raise est bien pratique lorsque vous souhaitez stopper l'exécution d'un programme si une variable ne se trouve pas dans le bon intervalle ou ne contient pas la bonne valeur. Vous avez sans doute compris maintenant pourquoi on parlait de « levée » d'exception... Pour les non anglophones, allez voir ce que signifie raise en anglais ;-) !

Enfin, on peut aussi être très précis dans le message d'erreur. Observez la fonction download_page() qui, avec le module urllib, télécharge un fichier sur internet.

import urllib.request

def download_page(address):
    error = ""
    page = ""
    try:
        data = urllib.request.urlopen(address)
        page = data.read()
    except IOError as e:
        if hasattr(e, 'reason'):
            error =  "Cannot reach web server: " + str(e.reason)
        if hasattr(e, 'code'):
            error = f"Server failed {e.code:d}"
    return page, error

data, error = download_page("https://files.rcsb.org/download/1BTA.pdb")

if error:
    print(f"Erreur rencontrée : {error}")
else:
    with open("proteine.pdb", "w") as prot:
        prot.write(data.decode("utf-8"))
    print("Protéine enregistrée")

La variable e est une instance de l'exception IOError. Certains de ses attributs sont testés avec la fonction hasattr() pour ainsi affiner le message renvoyé (ici contenu dans la variable error).

Si tout se passe bien, la page est téléchargée est stockée dans la variable data, puis ensuite enregistrée sur le disque dur.

Pour aller plus loin sur les exceptions, vous pouvez lire l'excellente page du blog impertinent et politiquement incorrect de Sam et Max.

21.3 Shebang et /usr/bin/env python3

Lorsque l'on programme sur un système Unix (Mac OS X ou Linux par exemple), on peut exécuter directement un script Python, sans appeler explicitement la commande python.

Pour cela, deux opérations sont nécessaires :

Étape 1. Préciser la localisation de l'interpréteur Python en indiquant dans la première ligne du script :

#! /usr/bin/env python

Par exemple, si le script test.py contenait :

print("Hello World !")

il va alors contenir :

#!/usr/bin/env python

print("Hello World !")

Étape 2.. Rendre le script Python exécutable en lançant l'instruction :

$ chmod +x test.py

Remarque

La ligne #! /usr/bin/env python n'est pas considérée comme un commentaire par Python, ni comme une instruction Python d'ailleurs . Cette ligne a une signification particulière pour le système d'exploitation Unix.

Pour exécuter le script, il suffit alors de taper son nom précédé des deux caractères ./ (afin de préciser au shell où se trouve le script) :

$ ./test.py
Hello World !

Définition

Le shebang correspond aux caractères #! qui se trouvent au début de la première ligne du script test.

Le shebang est suivi du chemin complet du programme qui interprète le script ou du programme qui sait où se trouve l'interpréteur Python. Dans l'exemple précédent, c'est le programme /usr/bin/env qui indique où se trouve l'interpréteur Python.

21.4 Passage d'arguments avec *args et **kwargs

Avant de lire cette rubrique, nous vous conseillons de bien relire et maîtriser la rubrique Arguments positionnels et arguments par mot-clé du chapitre 9 Fonctions.

Dans le chapitre 9, nous avons vu qu'il était nécessaire de passer à une fonction tous les arguments positionnels définis dans celle-ci. Il existe toutefois une astuce permettant de passer un nombre arbitraire d'arguments positionnels :

>>> def fct(*args):
...     print(args)
...
>>> fct()
()
>>> fct(1)
(1,)
>>> fct(1, 2, 5, "Python")
(1, 2, 5, 'Python')
>>> fct(z=1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: fct() got an unexpected keyword argument 'z'

L'utilisation de la syntaxe *args permet d'empaqueter tous les arguments positionnels dans un tuple unique args récupéré au sein de la fonction. L'avantage est que nous pouvons passer autant d'arguments positionnels que l'on veut. Toutefois, on s'aperçoit en ligne 10 que cette syntaxe ne fonctionne pas avec les arguments par mot-clé.

Il existe un équivalent avec les arguments par mot-clé :

>>> def fct(**kwargs):
...     print(kwargs)
...
>>> fct()
{}
>>> fct(z=1, gogo="toto")
{'gogo': 'toto', 'z': 1}
>>> fct(z=1, gogo="toto", y=-67)
{'y': -67, 'gogo': 'toto', 'z': 1}
>>> fct(1, 2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: fct() takes 0 positional arguments but 2 were given

La syntaxe **kwargs permet d'empaqueter l'ensemble des arguments par mot-clé, quel que soit leur nombre, dans un dictionnaire unique kwargs récupéré dans la fonction. Les clés et valeurs de celui-ci sont les noms des arguments et les valeurs passées à la fonction. Toutefois, on s'aperçoit en ligne 9 que cette syntaxe ne fonctionne pas avec les arguments positionnels.

Si on attend un mélange d'arguments positionnels et par mot-clé, on peut utiliser *args et **kwargs en même temps :

>>> def fct(*args, **kwargs):
...     print(args)
...     print(kwargs)
...
>>> fct()
()
{}
>>> fct(1, 2)
(1, 2)
{}
>>> fct(z=1, y=2)
()
{'y': 2, 'z': 1}
>>> fct(1, 2, 3, z=1, y=2)
(1, 2, 3)
{'y': 2, 'z': 1}

Deux contraintes sont toutefois à respecter. Il faut toujours :

  • mettre *args avant **kwargs dans la définition de la fonction ;
  • passer les arguments positionnels avant ceux par mot-clé lors de l'appel de la fonction.

Enfin, il est possible de combiner des arguments positionnels avec *args et **kwargs, par exemple :

def fct(a, b, *args, **kwargs):

Dans un tel cas, il faudra obligatoirement passer les deux arguments a et b à la fonction, ensuite on pourra mettre un nombre arbitraire d'arguments positionnels (récupérés dans le tuple args), puis un nombre arbitraire d'arguments par mot-clé (récupérés dans le dictionnaire kwargs).

Conseils

Les noms *args et **kwargs sont des conventions en Python, ils rappellent les mots arguments et keyword arguments. Bien qu'on puisse mettre ce que l'on veut, nous vous conseillons de respecter ces conventions pour faciliter la lecture de votre code par d'autres personnes.

L'utilisation de la syntaxe *args et **kwargs est très classique dans le module Tkinter présenté dans le chapitre 20.

Enfin, il est possible d'utiliser ce mécanisme d'empaquetage / désempaquetage (packing / unpacking) dans l'autre sens :

>>> def fct(a, b, c):
...    print(a,b,c)
...
>>> t = (-5,6,7)
>>>
>>> fct(*t)
-5 6 7

Avec la syntaxe *t on désempaquette le tuple à la volée lors de l'appel à la fonction. Cela est aussi possible avec un dictionnaire :

>>> def fct(x, y, z):
...    print(x, y, z)
...
>>> dico = {'x': -1, 'y': -2, 'z': -3}
>>> fct(**dico)
-1 -2 -3

Attention toutefois à bien respecter deux choses :

  • la concordance entre le nom des clés du dictionnaire et le nom des arguments dans la fonction (sinon cela renvoie une erreur) ;
  • l'utilisation d'une double étoile pour désempaqueter les valeurs du dictionnaire (si vous utilisez une seule étoile, Python désempaquettera les clés !).

21.5 Un peu de transformée de Fourier avec NumPy

La transformée de Fourier est très utilisée pour l'analyse de signaux, notamment lorsqu'on souhaite extraire des périodicités au sein d'un signal bruité. Le module NumPy possède la fonction fft() (dans le sous-module fft) permettant de calculer des transformées de Fourier.

Voici un petit exemple sur la fonction cosinus de laquelle on souhaite extraire la période à l'aide de la fonction fft() :

import numpy as np

debut = -2 * np.pi
fin = 2 * np.pi
pas = 0.1
x = np.arange(debut,fin,pas)
y = np.cos(x)

TF = np.fft.fft(y)
ABSTF = np.abs(TF)
pas_xABSTF = 1/(fin-debut)
x_ABSTF = np.arange(0,pas_xABSTF * len(ABSTF),pas_xABSTF)

Plusieurs commentaires sur cet exemple :

Ligne 1. On charge le module NumPy avec le nom raccourci np.

Lignes 3 à 6. On définit l'intervalle (de \(-2\pi\) à \(2\pi\) radians) pour les valeurs en abscisse ainsi que le pas (0,1 radians).

Lignes 7. On calcule directement les valeurs en ordonnées avec la fonction cosinus du module NumPy. On constate ici que NumPy redéfinit certaines fonctions ou constantes mathématiques de base, comme pi, cos() ou abs() (valeur absolue, ou module d'un nombre complexe). Ces fonctions sont directement utilisables avec un objet array.

Ligne 9. On calcule la transformée de Fourier avec la fonction fft() qui renvoie un vecteur (objet array à une dimension) de nombres complexes. Eh oui, le module NumPy gère aussi les nombres complexes !

Ligne 10. On extrait le module du résultat précédent avec la fonction abs().

Ligne 11. La variable x_ABSTFL représente l'abscisse du spectre (en radian\(^{-1}\)).

Ligne 12. La variable ABSTF contient le spectre lui même. L'analyse de ce dernier nous donne un pic à 0,15 radian\(^{-1}\), ce qui correspond bien à \(2\pi\) (c'est plutôt bon signe de retrouver ce résultat).

21.6 Sauvegardez votre historique de commandes

Vous pouvez sauvegarder l'historique des commandes utilisées dans l'interpréteur Python avec le module readline.

>>> print("hello")
hello
>>> a = 22
>>> a = a + 11
>>> print(a)
33
>>> import readline
>>> readline.write_history_file()

Quittez Python. L'historique de toutes vos commandes est dans votre répertoire personnel, dans le fichier .history.

Relancez l'interpréteur Python.

>>> import readline
>>> readline.read_history_file()

Vous pouvez accéder aux commandes de la session précédente avec la flèche du haut de votre clavier. D'abord les commandes readline.read_history_file() et import readline de la session actuelle, puis print(a), a = a + 11, a = 22...