Quelques modules et built-in

Définition et utilisation de modules

Définition

Dans le chapitre précédent, l’ensemble des extraits de code ont été saisis de manière interactive. Cette méthode n’est pas viable pour les traitements que l’on exécute plusieurs fois ou que l’on souhaite exécuter sur plusieurs machines. Pour rendre le code persistant, la première solution est d’écrire un «programme», c’est-à-dire de saisir le code dans un fichier texte avec l’extension .py. Un programme peut ainsi être exécuté plusieurs fois. Cependant, même si ce programme est correctement structuré avec des fonctions, il n’est pas possible de réutiliser facilement ces dernières (si ce n’est avec un copier-coller qui est une abomination).

Pour capitaliser les développement, Python propose la notion de module. Un module permet de fournir des bibliothèques de fonctions, structures de données, classes, à intégrer dans les programmes. Dans le cas de python, produire un module est identique à produire un programme: faire un fichier. Les définitions contenues dans un fichier sont utilisable globalement ou unitairement. Ainsi, le fichier examples.py peut être utilisé comme un module (nommé comme le fichier) et offrir l’accès aux deux fonctions à tout programme en ayant l’utilité.

La chaîne de documentation doit être mise avant toute déclaration (c’est-à-dire aussi avant les clauses import) pour être considérée comme documentation du module. Les deux premières lignes de ce fichier (optionnelles) ont la significations suivante:

  • la première ligne de ce fichier indique que ce fichier sera reconnu comme un programme Python par un environnement Unix (s’il a les droits en exécution) [1];
  • la seconde ligne précise le type d’encodage du fichier: ici l’encodage utf-8 supportant la gestion des accents. Dès qu’un fichier Python contient un accent, cette ligne est obligatoire (dans le cas contraire elle peut être omise).
#! /usr/bin/env python
# -*- coding: utf-8 -*-
#
# examples.py
#

"""
Regroupe les définitions des fonctions relatives au chapitre 1 de
`Initiation a python par l'exemple'.
"""

def fib(n):
    """Calcule la suite de Fibonacci jusque n"""
    a, b = 0, 1
    while b < n:
        print b,
        a, b = b, a + b

def welcome(name, greeting='Hello', mark='!'):
    """Un hello world configurable"""
    print greeting, name, mark

Utilisation d’un module

La construction import permet d’importer un module et de fournir accès à son contenu [2]. L’importation d’un module peut se faire de deux manières. La première solution est de désigner le module que l’on souhaite utiliser, son contenu est alors utilisable de manière «scopée», c’est-à-dire en préfixant le nom de la fonctionnalité du nom du module (ligne 2). La seconde solution repose sur la construction from import où l’on identifie la ou les fonctionnalités que l’on souhaite importer d’un module (ligne 4). Dans le cas d’un import de plusieurs fonctionnalités, les noms sont séparés par des virgules. L’utilisation se fait alors sans préfixer par le nom de module («non scopé»). Enfin, il est possible d’importer, avec cette seconde approche, tous les éléments d’un module en utilisant la notation * (ligne 7). Attention, avec cette dernière forme car il y a pollution du namespace global: deux imports consécutifs peuvent charger deux définition d’une même fonction. La seconde masquera la première.

>>> import examples
>>> examples.welcome('raphael')
Hello raphael !
>>> from examples import welcome
>>> welcome('raphael')
Hello raphael !
>>> from examples import *
>>> fib(100)
1 1 2 3 5 8 13 21 34 55 89

Utilisation mixte

Un fichier peut à la fois définir un module et un programme. Cette définition mixte est intéressante pour, par exemple, utiliser les fonctionnalités d’un programme indépendamment de celui-ci, ou pour associer le code de test à un module. Dans ce cas, la fin du fichier contient la définition de l’initialisation du code comme un programme. Pour cela, un test est réalisé afin de savoir si le code est importé ou exécuté. Si le code est importé, la variable __name__ contient le nom du module, sinon elle contient la chaîne __main__. Ici, dans le cas d’une exécution certaines des fonctions du module sont testées. (Sinon il ne se passe rien.)

if __name__ == '__main__':
    welcome('world')
    welcome('monde', 'Bonjour')
    welcome('world', mark='...')
    fib(100)

L’utilisation du fichier examples comme un programme donne la trace d’exécution suivante. Que l’on utilise explicitement l’interprète ou non (du fait de la première ligne du fichier) la trace est identique.

$ python examples.py
Hello world !
Bonjour monde !
Hello world ...
1 1 2 3 5 8 13 21 34 55 89
$ ls -l
total 100
-rwxr-x---  1 raphael raphael  1046 2005-03-11 11:51 examples.py*
$ ./examples.py
Hello world !
Bonjour monde !
Hello world ...
1 1 2 3 5 8 13 21 34 55 89

Remarques

Lors de l’utilisation de la construction import, l’interprète recherche la disponibilité des modules demandé dans le chemin décrit par la variable d’environnement PYTHONPATH. La valeur par défaut de cette variable contient le répertoire d’installation de Python et le répertoire courant.

Lors de l’utilisation de la construction from module import *, tout le contenu du module est importé à l’exception des définition dont le nom commence par un _ (qui reflètent la notion de privé en Python). L’utilisation de ce type d’importation allège le code saisi, ce qui est intéressant en interactif. Toutefois, une fois le préfixe par le nom de module perdu il peut de nouveau y avoir des conflits de noms entre les fonctionnalités importées depuis plusieurs modules.

Enfin, les modules peuvent être organisés selon une structure hiérarchique. Dans ce cas, les modules contenant des sous-modules, encore nommés packages, sont définis comme des répertoires sur le système de fichier. Le nommage des modules se fait alors par concaténation: mod.submod. Afin de faciliter la portabilité du code, tout répertoire doit contenir un fichier nommé __init__.py. Pour permettre le chargement de tous les sous-modules, ce fichier doit contenir la liste des modules du package (répertoire) stockée dans la variable __all__.

$ cat graphical/__init__.py
__all__ = ['basic', 'advanced']

Quelques modules standards

Python offre un grand nombre de modules. Ne sont présentés ici que les quatre modules considérés comme incontournables (C’est-à-dire ceux que l’auteur utilise couramment):

  • sys fournit les paramètres et fonctions liées à l’environnement d’exécution,
  • string fournit des opérations courantes sur les chaînes de caractères (équivalentes aux méthodes de la classe string plus quelques bonus),
  • re fournit le support des expressions régulières pour faire du pattern matching et des substitutions,
  • os fournit l’accès aux services génériques du système d’exploitation.

Pour chacun de ces modules, les fonctionnalités de base sont présentées et illustrées sur des exemples.

Le module sys

Quelques constantes

argv Séquence des paramètres passé sur la ligne de commande (argv[0] représente le nom du script).

stdin stdout stderr Objets de type file (fichier) représentant les entrées et sorties standard. Ces objets peuvent être remplacés par tout objet offrant une méthode write.

path Séquence contenant les chemins de la variable d’environnement PYTHONPATH. Cette séquence peut être manipulée dynamiquement.

>>> sys.path.append('/tmp/python')
['', '/home/raphael', '/usr/lib/python2.6', '/usr/lib/python2.6/plat-linux2',
 '/usr/lib/python2.6/lib-tk', '/usr/lib/python2.6/lib-old',
 '/usr/lib/python2.6/lib-dynload', '/usr/lib/python2.6/dist-packages',
 '/var/lib/python-support/python2.6', '/usr/local/lib/python2.6/dist-packages',
 '/tmp/python']

platform Nom du système d’exploitation.

# sous linux
>>> sys.platform
'linux2'

# sous OSX
>>> sys.platform
'darwin'
  • ps1, ps2 Variables contenant les valeurs des prompts, par défaut ‘>>>‘ et ‘...‘.

Quelques fonctions

exit([arg]) Mettre fin à l’exécution d’un programme, arg étant le statut de sortie pour le système d’exploitation. Cette fonction respecte le nettoyage des clauses finally (voir section ref{sec:chap3:exceptions}).

Le module string

Ce module fournit un certain nombre de constantes et de fonctions de manipulation des chaînes de caractères. Il est généralement recommandé d’utiliser les méthodes disponibles sur les objets de type string équivalentes.

>>> import string

Quelques constantes

Des constantes pratiques offertes par le module string définissent des ensembles de caractères:

>>> string.lowercase
'abcdefghijklmnopqrstuvwxyz'
>>> string.uppercase
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
>>> string.letters
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
>>> string.digits
'0123456789'
>>> string.whitespace
'\t\n\x0b\x0c\r '
>>> string.punctuation
'!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'

Les fonctions principales

lower upper cap* permettent de gérer la casse d’une chaîne de caractères: mise en minuscules, en majuscules, capitalisation de la phrase ou des mots (avec la réduction des espaces).

>>> string.lower('FOO')
'foo'
>>> string.upper('foo')
'FOO'
>>> string.capitalize('foo')
'Foo'
>>> string.capwords(' hello    world! ')
'Hello World!'

strip expandtabs permettent de gérer les espaces dans une chaîne de caractères en supprimant les blancs non significatifs ou en remplaçant les tabulations par un nombre fixe d’espaces.

>>> string.strip('  hello  world! \n ') # [2nd arg]
'hello  world!'
>>> string.expandtabs('\thello world!', 4)
'    hello world!'

find permet de rechercher un motif dans une chaîne (à partir du début ou de la fin pour rfind) en retournant l’indice où le motif est trouvé pour la première fois ou -1. (La fonction index est similaire mais lève une exception pour un motif non trouvé.)

>>> string.find('bonjour le monde', 'on')
1
>>> string.rfind('bonjour le monde', 'on')
12
>>> string.rfind('bonjour le monde', 'om')
-1

split permet de découper une chaîne en une liste de mots (par défaut sur les espaces, ou alors sur une chaîne passée en second argument) et join permet l’opération inverse qui est de construire une chaîne à partir d’une liste de mots et une chaîne de liaison.

>>> string.split('foo bar 123')
['foo', 'bar', '123']
>>> string.split("hello world", "wo")
['hello ', 'rld']
>>> string.join(['foo', 'bar', '123'], ';')
'foo;bar;123'

count replace permettent respectivement de compter les occurrence d’un motif dans une chaîne et le remplacement de ce motif par un autre.

>>> string.count('bonjour le monde', 'on')
2
>>> string.replace('bonjour le monde', 'on', 'ONON')
'bONONjour le mONONde'

zfill center rjust ljust permettent de gérer l’affichage: zfill complète un nombre avec des zéros en tête pour une largeur d’affichage constant, les trois autres fonctions permettent de justifier une phrase sur une largeur donnée.

>>> string.zfill(str(123), 5)
'00123'
>>> string.center('hi!', 10)
'   hi!    '
>>> string.rjust('hi!', 10)
'       hi!'
>>> string.ljust('hi!', 10)
'hi!       '

Le module re

Ce module permet la manipulation des expressions régulières [3]. Ces expressions sont par défaut identiques à Perl et l’ouvrage «Mastering Regular Expressions» de Jeffrey Friedl (O’Reilly) est un bon complément pour qui doit utiliser les expressions régulières. Afin de ne pas devoir toujours utiliser des ‘\‘ dans les expressions, il est possible d’utiliser des Raw regular expressions en préfixant la chaîne définissant l’expression par un ‘r‘: r'(.*)\n'.

>>> import re

Opérations de recherche

search(pattern, chaine) permet de rechercher le pattern dans la chaîne et retourne un objet de type SRE_Match décrivant la réponse ou bien None. La classe SRE_Match fournit des méthodes pour obtenir l’indice de début (start()) et de fin (end()) du motif dans la chaîne. Ici, la méthode matching utilise span() qui fournit un tuple contenant les deux indices.

>>> def matching(res):
...     if res: print res.span()
...     else: print 'no matching'
>>> matching(re.search('ada', 'abracadabra'))
(5, 8)

match(pattern, chaine) test si la chaîne commence par le pattern et retourne un objet de type SRE_Match décrivant la réponse ou bien None.

>>> res = re.match('abr', 'abracadabra')
>>> matching(res)
(0, 3)

Opérations de manipulation

split(pattern, chaine) découpe la chaîne par rapport au pattern. L’exemple suivant fait un découpage en mots (tout ce qui n’est pas espace ou ponctuation).

>>> re.split('\W+', 'Hello world !')
['Hello', 'world', '']

sub(pattern, repl, chaine) retourne une chaîne ou toutes les occurrences du pattern dans la chaîne fournie sont remplacés par repl. La chaîne n’est pas modifiée, une copie est créée [4].

>>> re.sub('hello', 'bonjour', 'hello foo hello bar')
'bonjour foo bonjour bar'

Utilisation d’expressions compilées

Lorsqu’une expression régulière est utilisée de manière répétitive dans un programme, il devient intéressant de compiler le motif à rechercher. Les fonctionnalités sont similaires à celles vues précédemment (méthodes) mais la performance est meilleure.

compile permet la création d’un objet représentant l’expression régulière sous forme compilée. Un fois l’objet créé, les fonctions sont appelées sur cet objet et non par rapport au module. La méthode group() sur l’objet SRE_Match donne accès au i-ème motif (dans le cas d’une expression composée de plusieurs motifs [5] comme ici) trouvé par la fonction search.

>>> exp = re.compile('<a href="(.*)">(.*)</a>')
>>> res = exp.search('Cliquer <a href="foo.html">ici</a>!')
>>> matching(res)
(8, 34)
>>> res.group(0)
'<a href="foo.html">ici</a>'
>>> res.group(1)
'foo.html'
>>> res.group(2)
'ici'

>>> exp = re.compile(r'.*<a href="(.*)">.*</a>.*')
>>> exp.sub(r'\1', 'Cliquer <a href="foo.html">ici</a>!')
'foo.html'

Le module os

Ce module permet d’accéder aux fonctions des systèmes d’exploitation et de faire de la programmation ou de l’administration système. Les fonctions sont disponibles sur (presque) tous les système. Toutefois, il faut toujours être conscient des limites pour la portabilité du code: la notion de droits sur un fichier n’est pas similaire sur tous les systèmes.

Les principaux sous modules de os sont:

  • path manipulation de chemins,
  • glob fnmatch pattern matching de fichiers et répertoires,
  • time accès et manipulation de l’heure,
  • getpass manipulation de mots de passe et identifiants. utilisateur
>>> import os

Quelques constantes

name donne le nom de l’implémentation du module os: posix, nt, dos, mac, java, etc.

>>> os.name
'posix'

environ est un dictionnaire contenant les variables d’environnement (au sens Unix mais aussi relatives au lancement de l’interprète Python).

>>> os.environ
{'USER': 'raphael', 'HOME': '/home/raphael',
'PATH': '/bin:/usr/bin:/opt/bin:/home/raphael/bin',
'HOSTNAME': 'alfri.lifl.fr',
'PWD': '/home/raphael/enseign/python/scripts'}

Le module os.path

Ce module fournit quelques primitives (courantes sous les environnements Unix) de manipulation des noms de fichiers et des fichiers.

basename dirname permettent respectivement d’extraire d’un chemin le nom du fichier ou le nom du répertoire (ou de l’arborescence dans le cas d’un chemin composé).

>>> os.path.basename('/tmp/foo.txt')
'foo.txt'
>>> os.path.dirname('/tmp/foo.txt')
'/tmp'

split join permettent respectivement de découper et de construire un chemin. L’utilisation de la fonction join est fortement recommandée pour construire des chemin car elle respecte implicitement le bon séparateur de chemin pour le système courant.

>>> os.path.split('/tmp/foo.txt')
('/tmp', 'foo.txt')
>>> os.path.join('/tmp', 'foo.txt')
'/tmp/foo.txt'

exists isdir isfile permettent de tester l’existence et le type d’un fichier (ou répertoire).

>>> os.path.exists('/tmp/bar.txt')
False
>>> os.path.isdir('/tmp')
True
>>> os.path.isfile('/tmp')
False

Les modules glob et fnmatch

Ces deux modules fournissent principalement une fonction portant le même nom.

glob est l’équivalent du ls Unix. Cette fonction retourne une liste des fichiers d’un répertoire avec utilisation de jockers.

>>> import glob
>>> glob.glob('*.py')
['hello.py', 'examples.py']
>>> glob.glob('/tmp/*.tmp')
['/tmp/sv3e2.tmp', '/tmp/sv001.tmp', '/tmp/sv3e4.tmp']

fnmatch et filter permettent de faire du pattern matching sur des chaînes de caractères représentant des noms de fichiers: respectivement est ce qu’un nom suit un pattern de nommage et quels sont les éléments d’une liste respectant un pattern.

>>> import fnmatch
>>> fnmatch.fnmatch('examples.py', '*.py')
True
>>> fnmatch.filter(['examples.py', 'hello.pyc'], '*.py')
['examples.py']

Le module getpass

Ce module permet de demander au système le nom de l’utilisateur connecté et de demander de manière «cachée» un mot de passe.

>>> import getpass

getuser() demande au système le nom de login de l’usager.

>>> getpass.getuser()
'raphael'

getpass() demande proprement (en masquant les caractères saisis) un mot de passe à l’usager. L’appel à cette fonction est bloquant jusqu’à ce que l’usager ait tapé entrée.

>>> p = getpass.getpass() # bloquant jusqu'au '\n'
Password:
>>> print p
'quelmauvaismotdepasse'

Built-in en Python

Les built-in sont les fonctionnalités câblées en dur dans l’interprète Python, et dans un certain sens l’interprète lui-même. Ces fonctionnalités ne sont pas écrite en Python car elles sont plus que largement utilisées et leur mise en {oe}uvre en C contribue à obtenir de meilleures performances.

Les fichiers

Les objets fichiers

Les fichiers sont représentés comme des objets de type file. Ils peuvent être textuels ou binaires et être utilisés en lecture ou en écriture.

open ouvre un fichier (crée l’objet associé) en lecture par défaut.

>>> foo = open('/tmp/foo.txt')
>>> foo
<open file '/tmp/foo.txt', mode 'r' at 0x...>

close ferme un fichier (mais ne détruit pas l’objet associé).

>>> foo.close()
>>> foo
<closed file '/tmp/foo.txt', mode 'r' at 0x...>

Lecture dans un fichier

Il y a plusieurs manières de lire un fichier.

readline() permet de lire une ligne (position courante jusqu’au prochain \n) à la fois dans le fichier.

>>> foo = open('/tmp/foo.txt', 'r')
>>> foo.readline()
'hello world!\n'

readlines() permet de lire toutes les lignes d’un fichier en une seule fois (retourne une séquence de chaînes).

>>> foo.readlines()
['bonjour le monde!\n', 'au revoir le monde!\n']

read([n]) permet de lire tout le fichier à partir de la position courante, ou au maximum n octets lorsque un argument est donné. Cette fonction retourne une chaîne. La fonction seek permet de se déplacer dans le fichier de façon absolue: ici nous retournons au début (indice 0).

>>> foo.seek(0) ; foo.read()
'hello world!\nbonjour le monde!\nau revoir le monde!\n'
>>> foo.close()

Comparaison des approches de lecture

Il n’y a pas de solution idéale pour la lecture des fichiers. Il faut choisir en fonction de la situation et des besoins.

  • La lecture globale (readlines) d’un fichier est plus performante pour l’aspect récupération des informations car elle représente un gros accès disque puis un parcours de séquence en mémoire. Toutefois, cette approche est coûteuse en mémoire vive: imaginez la présence d’un fichier texte de 500Mo en mémoire.
  • Lecture ligne par ligne (readline) est plus coûteuse pour lire un fichier car elle représente de nombreux accès disques pour des petites quantités d’information. Toutefois, cette approche permet de manipuler des gros fichiers: un fichier de 10Go peut être lu ligne par ligne sur une machine ayant 64Mo de mémoire.

Depuis Python 2.3, il est possible d’itérer simplement ligne par ligne sur un fichier ouvert (sans avoir à utiliser la fonction readline). Cette manière de faire repose sur les itérateurs (voir la section ref{ssub:chap1:iterateurs}).

>>> fichier = open('/tmp/foo.txt')
>>> for line in fichier:
...     print line.strip()
...
hello world!
bonjour le monde!
au revoir le monde!

>>> fichier.close()

Ecriture dans un fichier

Il faut qu’un fichier soit ouvert en écriture pour pouvoir écrire dedans, on le précise donc à l’ouverture en donnant un second paramètre 'w' (Python suit les modes d’ouverture du langage C, pour les fichiers binaires il faut préciser 'b' en plus). Tant que le fichier n’est pas fermé, son contenu n’est pas garanti sur le disque.

write permet d’écrire des données (une chaîne de texte) représentant une ou plusieurs lignes de texte (utilisation de ‘\n‘). Les donnés peuvent aussi être binaires.

>>> foo = open('/tmp/foo.txt', 'w')
>>> foo.write('hello world!\n')

writelines permet d’écrire des données contenues dans une séquence (de chaînes). Si chaque chaîne contenue dans la séquence représente une ligne de texte dans le fichier, il faut alors qu’elles contiennent toute la séquence de fin de ligne `\n‘. Dans le cas contraire, tout sera écrit dans le fichier, mais la notion de ligne sera définie par les \n contenus (même s’ils n’étaient pas en fin d’une des chaînes).

>>> lines = ['bonjour le monde!\n', 'au revoir le monde!\n']
>>> foo.writelines(lines)
>>> foo.close()

Conversions de types

Utilisation du nom des types pour convertir les chaînes, en entiers, flottants, etc. (et réciproquement.) La fonction str permet de convertir tout objet en chaîne.

>>> str(123)
'123'
>>> int('123')
123
>>> float('123')
123.0
>>> float(123)
123.0
>>> long('123')
123L

Evaluation dynamique

Python permet d’exécuter des commandes à la volée [6]: une chaîne de caractères représentant une commande est exécutée. La chaîne peut être compilée explicitement ou non avant son évaluation. Cette possibilité permet de faire de la génération dynamique de commandes.

compile() retourne une version compilée de l’expression (ou du fichier) passé en paramètre. Le second argument précise où doit se faire la sortie d’erreur au cas où une exception serait levée. Le dernier argument précise quel usage est envisagé, ici nous prévoyons de l’utiliser avec la fonction eval().

>>> obj = compile('x + 1', '<string>', 'eval')
>>> obj
<code object <module> at 0x... file "<string>", line 1>

eval() évalue une expression (compilée ou non) passé en paramètre.

>>> x = 2
>>> eval('x + 1')
3
>>> eval(obj)
3

Assertions

Les assertions permettent de traiter les situations sans appel: soit la condition est respectée, soit le programme est arrêté. (Dans la pratique, une exception est levée.)

assert évalue une expression logique et arrête le programme en affichant un message d’erreur (qui est optionnel et fourni séparé par une virgule) si l’expression logique est fausse (cf section ref{sub:chap1:tests}). L’expression peut inclure des appels de fonctions.

>>> assert 1
>>> assert 0, 'oups'
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
AssertionError: oups
>>> try: assert 0
... except AssertionError: print 'sic'
...
sic

Exercices

Utilitaire de sauvegarde

En s’aidant de la fonction os.stat qui permet de connaître la dates de modification des fichiers, écrire un utilitaire de backup d’une arborescence. Cet utilitaire doit faire un backup incrémental (sauf la première fois) des fichiers modifiés.

>>> help(os.stat)
Help on built-in function stat in module posix:

stat(...)
    stat(path) -> stat result

    Perform a stat system call on the given path.

Extraction de données

Cet exercice propose d’utiliser des fonctions du module regular exressions pour extraire des données d’un fichier texte. Ecrire un programme analysant un fichier texte de type mbox et qui produit la liste de toutes les personnes ayant envoyé un e-mail, en triant la liste de leurs adresses par ordre alphabétique avec la date de l’e-mail reçu le plus récent.

[1]La construction #! sert à préciser, sous Unix, le programme à utiliser pour exécuter un script.
[2]Nous considérons ici que l’interprète a été lancé dans le répertoire contenant le fichier examples.py.
[3]Voir http://www.amk.ca/python/howto/regex/
[4]En Python les chaînes sont des objets immuables, toute «modification» d’une chaîne entraîne en fait la création d’une nouvelle chaîne contenant la version «modifiée».
[5]Un motif au sein d’une expression régulière est défini par des parenthèses.
[6]Bien que tout est dynamiquement évalué en python, il est possible de faire des choses encore plus dynamiquement, comme de la production de code exécutable à la volée.