Une introduction à Python 3

Image non disponible


précédentsommairesuivant

9. Quelques techniques avancées de programmation

Image non disponible

Ce chapitre présente quelques exemples de techniques avancées dans les trois paradigmes que supporte Python, les programmations procédurale, objet et fonctionnelle.

9-1. Techniques procédurales

9-1-1. Le pouvoir de l'introspection

C'est un des atouts de Python. On entend par introspection la possibilité d'obtenir des informations sur les objets manipulés par le langage.

9-1-1-a. La fonction help()

On peut tout d'abord utiliser la fonction prédéfinie help().

Cette fonction est autodocumentée :

 
Sélectionnez
>>> help()
Welcome to Python 3.4 help utility!
If this is your first time using Python, you should definitely check out
the tutorial on the Internet at http://docs.python.org/3.4/tutorial/.
Enter the name of any module, keyword, or topic to get help on writing
Python programs and using Python modules. To quit this help utility and
return to the interpreter, just type "quit".
To get a list of available modules, keywords, symbols, or topics, type 
"modules","keywords","symbols", or "topics". Each module also comes
with a one-line summary of what it does; to list the modules whose name
or summary contain a given string such as "spam", type "modules spam".
help> quit
You are now leaving help and returning to the Python interpreter.
If you want to ask for help on a particular object directly from the
interpreter, you can type "help(object)". Executing "help('string')"
has the same effect as typing a particular string at the help> prompt.

L'interpréteur IPython propose une aide encore plus efficace grâce à son mécanisme d'autocomplétion contextuelle (touche Image non disponible) :

 
Sélectionnez
In [1]: seq = []
In [2]: seq.
seq.append seq.copy seq.extend seq.insert seq.remove seq.sort
seq.clear seq.count seq.index seq.pop seq.reverse
In [2]: seq.sort?
Type: builtin_function_or_method
String form: <built-in method sort of list object at 0xb5e0c1ac>
Docstring: L.sort(key=None, reverse=False) -> None -- stable sort *IN PLACE*

La fonction utilitaire printInfo() filtre les méthodes disponibles de son argument ne commençant pas par _ et affiche les docstrings associées sous une forme plus lisible que help() :

 
Sélectionnez
def printInfo(object) :
    """Filtre les méthodes disponibles de <object>."""
    methods = [method for method in dir(object)
        if callable(getattr(object, method)) and not method.startswith('_')]
    
    for method in methods :
        print(getattr(object, method).__doc__)

Par exemple, l'appel printInfo([]) affiche la documentation :

 
Sélectionnez
L.append(object) -- append object to end
L.count(value) -> integer -- return number of occurrences of value
L.extend(iterable) -- extend list by appending elements from the iterable
L.index(value, [start, [stop]]) -> integer -- return first index of value.
Raises ValueError if the value is not present.
L.insert(index, object) -- insert object before index
L.pop([index]) -> item -- remove and return item at index (default last).
Raises IndexError if list is empty or index is out of range.
L.remove(value) -- remove first occurrence of value.
Raises ValueError if the value is not present.
L.reverse() -- reverse *IN PLACE*
L.sort(cmp=None, key=None, reverse=False) -- stable sort *IN PLACE*;
cmp(x, y) -> -1, 0, 1

9-1-1-b. Les fonctions type(), dir() et id()

Ces fonctions fournissent respectivement le type, tous les noms définis dans l'espace de noms et la localisation mémoire (unique) d'un objet :

 
Sélectionnez
>>> li = [1, 2, 3]
>>>
>>> type(li)
<class 'list'>
>>>
>>> dir(li)
['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__doc__', '__eq__', '__format__',
'__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__',
'__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__',
'__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__',
'append', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']
>>>
>>> id(li)
3074801164

9-1-1-c. Les fonctions locals() et globals()

Comme nos l'avons déjà vu chapitre 5.3.1Portée des objets, ces fonctions retournent respectivement le dictionnaire des noms locaux (globaux) au moment de leur appel.

9-1-1-d. Le module sys

Ce module fournit nombre d'informations générales, entre autres :

 
Sélectionnez
>>> import sys
>>>
>>> sys.executable
'/usr/bin/python3'
>>>
>>> sys.platform
'linux2'
>>>
>>> sys.version
'3.2.3 (default, Oct 19 2012, 20:13:42) \n[GCC 4.6.3]'
>>>
>>> sys.argv
['']
>>>
>>> sys.path
['', '/usr/lib/python3.2', '/usr/lib/python3.2/plat-linux2', '/usr/lib/python3.2/lib-dynload', '/usr/local/lib/python3.2/dist-packages', '/usr/lib/']
>>> sys.modules
{'reprlib': <module 'reprlib' from '/usr/lib/python3.2/reprlib.py'>, 'heapq': <module 'heapq' from '/usr/lib/python3.2/heapq.py'>,
'sre_compile': <module 'sre_compile' from '/usr/lib/python3.2/sre_compile.py'>,
…

9-1-2. Gestionnaire de contexte (ou bloc gardé)

Cette syntaxe simplifie le code en assurant que certaines opérations sont exécutées avant et après un bloc d'instructions donné. Illustrons ce mécanisme sur un exemple classique où il importe de fermer le fichier utilisé :

 
Sélectionnez
# au lieu de ce code :
try :
    fh = open(filename)
    for line in fh :
        process(line)
finally :
    fh.close()

# il est plus simple d'écrire :
with open(filename) as fh :
    for line in fh :
        process(line)

9-1-3. Utiliser un dictionnaire pour lancer des fonctions ou des méthodes

L'idée est d'exécuter différentes parties de code en fonction de la valeur d'une variable de contrôle. On peut se servir de cette technique pour implémenter un menu textuel :

 
Sélectionnez
animaux = []
nombre_de_felins = 0

def gererChat() :
    global nombre_de_felins
    print("Miaou")
    animaux.append("félin")
    nombre_de_felins += 1

def gererChien() :
    print("Ouah")
    animaux.append("canidé")

def gererOurs() :
    print("Attention au *OUILLE* !")
    animaux.append("plantigrade")

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

dico = {
    "chat" : gererChat,
    "chien" : gererChien,
    "ours" : gererOurs
}

betes = ["chat", "ours", "chat", "chien"] # une liste d'animaux rencontrés

for bete in betes :
    dico[bete]() # appel de la fonction correspondante

nf = nombre_de_felins
print("nous avons rencontré {} félin(s)".format(nf))
print("Les animaux rencontrés sont : {}".format(', '.join(animaux), end="")

"""
Miaou
Attention au *OUILLE* !
Miaou
Ouah
nous avons rencontré 2 félin(s)
Les animaux rencontrés sont : félin, plantigrade, félin, canidé
"""

9-1-4. Les fonctions récursives

Une fonction récursive comporte un appel à elle-même.

Plus précisément, une fonction récursive doit respecter les trois propriétés suivantes :

  1. Une fonction récursive contient un cas de base ;
  2. Une fonction récursive doit modifier son état pour se ramener au cas de base ;
  3. Une fonction récursive doit s'appeler elle-même.

Par exemple, trier un tableau de kitxmlcodeinlinelatexdvpNfinkitxmlcodeinlinelatexdvp éléments par ordre croissant, c'est extraire le plus petit élément puis trier le tableau restant à kitxmlcodeinlinelatexdvpN-1finkitxmlcodeinlinelatexdvp éléments.

Un algorithme classique très utile est la méthode de Horner qui permet d'évaluer efficacement un polynôme de degré kitxmlcodeinlinelatexdvpnfinkitxmlcodeinlinelatexdvp en une valeur donnée kitxmlcodeinlinelatexdvpx_0finkitxmlcodeinlinelatexdvp, en remarquant que cette réécriture ne contient plus que n multiplications :

kitxmlcodelatexdvpp(x_0) = ((\dots ((a_n x_0 + a_{n-1})x_0 + a_{n-2})x_0 + \dots)x_0 + a_1)x_0) + a_0finkitxmlcodelatexdvp

Voici une implémentation récursive de l'algorithme de Horner dans laquelle le polynôme kitxmlcodeinlinelatexdvppfinkitxmlcodeinlinelatexdvp est représenté par la liste de ses coefficients kitxmlcodeinlinelatexdvp[a_0, \dots, a_n]finkitxmlcodeinlinelatexdvp :

 
Sélectionnez
>>> def horner(p, x):
...     if len(p) == 1:
...         return p[0]
...     p[-2] += x * p[-1]
...     return horner(p[:-1], x)
...
>>> horner([5, 0, 2, 1], 2) # x**3 + 2*x**2 + 5, en x = 2
21

Les fonctions récursives sont souvent utilisées pour traiter les structures arborescentes comme les répertoires dans les systèmes de fichiers des disques durs.

Voici l'exemple d'une fonction qui affiche récursivement les fichiers d'un répertoire fourni en paramètre :

 
Sélectionnez
#-*- coding : utf8 -*-

from os import listdir
from os.path import isdir, join

def listeFichiersPython(repertoire) :
    """Affiche récursivement les fichiers Python à partir de <repertoire>."""
    noms = listdir(repertoire)
    for nom in noms :
        if nom in (".", "..") :
            continue
        nom_complet = join(repertoire, nom)
        if isdir(nom_complet) :
            listeFichiersPython(nom_complet)
        elif nom.endswith(".py") or nom.endswith(".pyw") :
            print("Fichier Python :", nom_complet)

listeFichiersPython("/home/bob/Tmp")

Dans cette définition, on commence par constituer dans la variable noms la liste des fichiers et répertoires du répertoire donné en paramètre. Puis, dans une boucle for, tant que l'élément examiné est un répertoire, on ré-appelle la fonction sur lui pour descendre dans l'arborescence de fichiers tant que la condition terminale if nom in (".", ".."): est fausse.

Le résultat produit est :

 
Sélectionnez
Fichier Python : /home/bob/Tmp/parfait_chanceux.py
Fichier Python : /home/bob/Tmp/recursif.py
Fichier Python : /home/bob/Tmp/parfait_chanceux_m.py
Fichier Python : /home/bob/Tmp/verif_m.py
Fichier Python : /home/bob/Tmp/Truc/Machin/tkPhone_IHM.py
Fichier Python : /home/bob/Tmp/Truc/Machin/tkPhone.py
Fichier Python : /home/bob/Tmp/Truc/calculate.py
Fichier Python : /home/bob/Tmp/Truc/tk_variable.py

9-1-4-a. La récursivité terminale

On dit qu'une fonction f est récursive terminale, si tout appel récursif est de la forme return f(…).

On parle alors d'appel terminal.

Python permet la récursivité, mais n'optimise pas automatiquement les appels terminaux. Il est donc possible(32) d'atteindre la limite arbitraire fixée à 1000 appels.

On peut pallier cet inconvénient de deux façons. Nous allons illustrer cette stratégie sur un exemple canonique, la factorielle.

La première écriture est celle qui découle directement de la définition de la fonction :

 
Sélectionnez
def factorielle(n) :
    """Version récursive non terminale."""
    if n == 0:
        return 1
    else :
        return n * factorielle(n-1)

On remarque immédiatement (return n * factorielle(n-1)) qu'il s'agit d'une fonction récursive non terminale. Or une fonction récursive terminale est en théorie plus efficace (mais souvent moins facile à écrire) que son équivalent non terminale : il n'y a qu'une phase de descente et pas de phase de remontée.

La méthode classique pour transformer cette fonction en un appel récursif terminal est d'ajouter un argument d'appel jouant le rôle d'accumulateur. D'où le code :

 
Sélectionnez
def factorielleTerm(n, accu=1) :
    """Version récursive terminale."""
    if n == 0:
        return accu
    else :
        return factorielleTerm(n-1, n*accu)

La seconde stratégie est d'essayer de transformer l'écriture récursive de la fonction par une écriture itérative. La théorie de la calculabilité montre qu'une telle transformation est toujours possible à partir d'une fonction récursive terminale, ce qu'on appelle l'opération de dérécursivation. D'où le code :

 
Sélectionnez
def factorielleDerec(n, accu=1) :
    """Version dérécursivée."""
    while n > 0:
        accu *= n
        n -= 1
    return accu

9-1-5. Les listes définies en compréhension

Les listes définies en compréhension, souvent appelées compréhension de listes, permettent de générer ou de modifier des collections de données par une écriture lisible, simple et performante.

Cette construction syntaxique se rapproche de la notation utilisée en mathématiques :

kitxmlcodeinlinelatexdvp\{x^2 | x \in[2,11[\} \Leftrightarrowfinkitxmlcodeinlinelatexdvp [x**2 for x in range(2, 11)] kitxmlcodeinlinelatexdvp\Rightarrowfinkitxmlcodeinlinelatexdvp [4, 9, 16, 25, 36, 49, 64, 81, 100]

Une liste en compréhension est équivalente à une boucle for qui construirait la même liste en utilisant la méthode append().

Les listes en compréhension sont utilisables sous trois formes.

Première forme expression d'une liste simple de valeurs :

 
Sélectionnez
result1 = [x+1 for x in une_seq]
# a le même effet que :
result2 = []
for x in une_seq :
    result2.append(x+1)

Deuxième forme expression d'une liste de valeurs avec filtrage :

 
Sélectionnez
result3 = [x+1 for x in une_seq if x > 23]
# a le même effet que :
result4 = []
for x in une_seq :
    if x > 23:
        result4.append(x+1)

Troisième forme expression d'une combinaison de listes de valeurs :

 
Sélectionnez
result5 = [x+y for x in une_seq for y in une_autre]
# a le même effet que :
result6 = []
for x in une_seq :
    for y in une_autre :
        result6.append(x+y)

Exemples utilisations très pythoniques :

 
Sélectionnez
valeurs_s = ["12", "78", "671"]

# conversion d'une liste de chaînes en liste d'entiers
valeurs_i = [int(i) for i in valeurs_s] # [12, 78, 671]

# calcul de la somme de la liste avec la fonction intégrée sum
print(sum([int(i) for i in valeurs_s])) # 761

# a le même effet que :
s = 0
for i in valeurs_s :
    s = s + int(i)
print(s) # 761

# Initialisation d'une liste 2D
multi_liste = [[0]*2 for ligne in range(3)]
print(multi_liste) # [[0, 0], [0, 0], [0, 0]]

Autre exemple :

 
Sélectionnez
>>> C_deg = range(-20, 41, 5)
>>> F_deg = [(9.0/5)*c + 32 for c in C_deg]
>>> table = [C_deg, F_deg]
>>> for i in range(len(table[0])):
...     print(table[0][i], "=>", table[1][i])
...
-20 => -4.0
-15 => 5.0
-10 => 14.0
-5 => 23.0
0 => 32.0
5 => 41.0
10 => 50.0
15 => 59.0
20 => 68.0
25 => 77.0
30 => 86.0
35 => 95.0
40 => 104.0

9-1-6. Les dictionnaires définis en compréhension

Comme pour les listes, on peut définir des dictionnaires en compréhension :

 
Sélectionnez
>>> {n : x**2 for n, x in enumerate(range(5))}
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

Notons l'utilisation des accolades et du deux-points caractéristiques de la syntaxe du dictionnaire.

9-1-7. Les ensembles définis en compréhension

De même, on peut définir des ensembles en compréhension :

 
Sélectionnez
>>> {n for n in range(5)}
set([0, 1, 2, 3, 4])

Dans ce cas les accolades sont caractéristiques de la syntaxe de l'ensemble.

9-1-8. Les générateurs et les expressions génératrices

9-1-8-a. Les générateurs

Un générateur est une fonction qui mémorise son état au moment de retourner une valeur.

La transmission d'une valeur s'effectue en utilisant le mot clé yield.

Les générateurs fournissent un moyen de générer des exécutions paresseuses, ce qui signifie qu'elles ne calculent que les valeurs réellement demandées. Ceci peut s'avérer beaucoup plus efficace (en termes de mémoire) que le calcul, par exemple, d'une énorme liste en une seule fois.

Techniquement, un générateur fonctionne en deux temps. D'abord, au lieu de retourner une valeur avec le mot clé return, la fonction qui doit servir de générateur produit une valeur et se met en pause dès qu'elle rencontre le mot clé yield.

Ensuite, à l'utilisation du générateur, le corps de la fonction est exécuté lors des appels explicites à la méthode next() ou implicites dans une boucle for.

Voici un exemple de générateur qui fournit un compteur d'entiers (initialisé à 0) inférieurs ou égaux à l'argument du générateur :

 
Sélectionnez
def counter(maximum) :
    """génère des entiers inférieurs ou égaux à <maximum>."""
    i = 0
    while True :
        yield i
        if i == maximum : # arrêt de générateur
            return
        i = i + 1

for val in counter(5) :
    print(val, end="")

Ce qui produit :

 
Sélectionnez
0 1 2 3 4 5

9-1-8-b. Les expressions génératrices

Une expression génératrice possède une syntaxe presque identique à celle des listes en compréhension ; la différence est qu'une expression génératrice est entourée de parenthèses.

9-1-8-c. Utilisation

Les expressions génératrices (souvent appelée « genexp ») sont aux générateurs ce que les listes en compréhension sont aux fonctions. Bien qu'il soit transparent, le mécanisme du yield vu ci-dessus est encore en action.

Par exemple l'expression suivante génère la création d'un million de valeurs en mémoire avant de commencer la boucle :

 
Sélectionnez
for i in [x**2 for x in range(1000000)]:

Alors que dans l'expression suivante, la boucle commence immédiatement et ne génère les valeurs qu'au fur et à mesure des demandes :

 
Sélectionnez
for i in (x**2 for x in range(1000000)) :

Voici un autre exemple : une genexp de recherche d'un motif dans un fichier, analogue à un grep simplifié(33) :

 
Sélectionnez
>>> with open('.bash_aliases') as f:
...     file = f.readlines()
...
>>> lines = (line.strip() for line in file if "alias" in line)
>>> for line in lines:
...     line
...
'# some more aliases'
"alias ll='ls -alF'"
"alias la='ls -A'"
"alias l='ls -CF'"
"alias c='clear'"
"alias p='~/ShellScripts/a2ps_UTF8'"
"alias ipq='ipython qtconsole &'"
"alias nb='ipython notebook &'"
"alias qt='ipython qtconsole &'"
'alias kie=\'find ~/Phi1/ -name "*.tex" | xargs grep\''

9-1-9. Les fonctions incluses

La syntaxe de définition des fonctions en Python permet tout à fait d'emboîter leur définition. Distinguons deux cas d'emploi :

  • idiome de la fonction fabrique renvoyant une fermeture :

     
    Sélectionnez
    >>> def creer_plus(ajout):
    ...     """Fonction 'fabrique'."""
    ...     def plus(increment):
    ...         """Fonction 'fermeture' : utilise des noms locaux à creer_plus()."""
    ...         return increment + ajout
    ...     return plus
    ...
    >>>
    >>> p = creer_plus(23)
    >>> q = creer_plus(42)
    >>>
    >>> print("p(100) =", p(100))
    ('p(100) =', 123)
    >>> print("q(100) =", q(100))
    ('q(100) =', 142)
  • fonction fabrique renvoyant une classe :
 
Sélectionnez
>>> class CasNormal:
...     def uneMethode(self):
...         print("normal")
...
>>> class CasSpecial:
...     def uneMethode(self):
...         print("spécial")
...
>>> def casQuiConvient(estNormal=True):
...     """Fonction fabrique renvoyant une classe."""
...     if estNormal:
...         return CasNormal()
...     else:
...         return CasSpecial()
...
>>>
>>> une_instance = casQuiConvient()
>>> une_instance.uneMethode()
normal
>>> une_instance = casQuiConvient(False)
>>> une_instance.uneMethode()
spécial

9-1-10. Les décorateurs

Les décorateurs permettent d'encapsuler un appel et donc d'effectuer des pré ou des posttraitements lors de l'appel d'une fonction, d'une méthode ou d'une classe.

Soit deco() un décorateur. Pour « décorer » une fonction on écrit :

 
Sélectionnez
def deco() :
    …

@deco
def fonction(arg1, arg2, ...) :
    pass

Une fonction peut être multidécorée :

 
Sélectionnez
def f1() :
    …

def f2() :
    …

def f3() :
    …

@f1 @f2 @f3
def g() :
    pass

Voici un exemple simple :

 
Sélectionnez
def unDecorateur(f) :
    cptr = 0
    def _interne(*args, **kwargs) :
        nonlocal cptr
        cptr = cptr + 1
        print("Fonction décorée :", f.__name__, ". Appel numéro :", cptr)
        return f(*args, **kwargs)
        
    return _interne

@unDecorateur
def uneFonction(a, b) :
    return a + b

def autreFonction(a, b) :
    return a + b

# programme principal ===============================================
## utilisation d'un décorateur
print(uneFonction(1, 2))
## utilisation de la composition de fonction
autreFonction = unDecorateur(autreFonction)
print(autreFonction(1, 2))

print(uneFonction(3, 4))
print(autreFonction(6, 7))
"""
Fonction décorée : uneFonction. Appel numéro : 1
3
Fonction décorée : autreFonction. Appel numéro : 1
3
Fonction décorée : uneFonction. Appel numéro : 2
7
Fonction décorée : autreFonction. Appel numéro : 2
13
"""

9-2. Techniques objets

Comme nous l'avons vu lors du chapitre précédent, Python est un langage complètement objet. Tous les types de base ou dérivés sont en réalité des types abstraits de données implémentés sous forme de classe.

9-2-1. Les Functors

En Python un objet fonction ou functor est une référence à tout objet appelable(34) : fonction, fonction anonyme lambda(35), méthode, classe. La fonction prédéfinie callable() permet de tester cette propriété :

 
Sélectionnez
>>> def maFonction():
...     print('Ceci est"appelable"')
...
>>> callable(maFonction)
True
>>> chaine = 'Une chaîne'
>>> callable(chaine)
False

Il est possible de transformer les instances d'une classe en functor si la méthode spéciale __call__() est définie dans la la classe :

 
Sélectionnez
>>> class A:
...     def __init__(self):
...         self.historique = []
...     def __call__(self, a, b):
...         self.historique.append((a, b))
...         return a + b
...
>>> a = A()
>>> a(1, 2)
3
>>> a(3, 4)
7
>>> a(5, 6)
11
>>> a.historique
[(1, 2), (3, 4), (5, 6)]

9-2-2. Les accesseurs

9-2-2-a. Le problème de l'encapsulation

Dans le paradigme objet, l'état d'un objet est privé, les autres objets n'ont pas le droit de le consulter ou de le modifier.

Classiquement, on distingue les visibilités suivantes :

  • publique ;
  • protégée ;
  • privée.
Image non disponible
Figure 9.1 - En Python, tous les attributs (données, méthodes) sont publics !

On peut néanmoins modifier cet état de fait.

Lorsqu'un nom est préfixé par un caractère souligné, il est conventionnellement réservé à un usage interne (privé). Mais Python n'oblige à rien(36), c'est au développeur de respecter la convention !

On peut également préfixer un nom par deux caractères souligné(37), ce qui permet d'éviter les collisions de noms dans le cas où un même attribut serait défini dans une sous-classe. Le renommage a comme effet de bord de rendre l'accès à cet attribut plus difficile de l'extérieur de la classe qui le définit, mais cette protection reste déclarative et n'offre pas une sécurité absolue.

9-2-2-b. La solution property

Le principe de l'encapsulation est mis en œuvre par la notion de propriété.

Une propriété (property) est un attribut d'instance possédant des fonctionnalités spéciales.

Deux syntaxes implémentent cette solution.

La première définit explicitement la propriété x et ses quatre paramètres (dans l'ordre : méthode de lecture, méthode de modification, méthode de suppression, chaîne de documentation) :

 
Sélectionnez
# !/usr/bin/python3
#-*- coding : utf-8 -*-
# fichier : property.py

class C :
    def __init__(self) :
        self._ma_propriete = None

    def getx(self) :
        """getter."""
        return self._x

    def setx(self, value) :
        """setter."""
        self._x = value

    def delx(self) :
        """deleter."""
        del self._x

    x = property(getx, setx, delx, "Je suis la propriété 'x'.")

# autotest =========================================================
if __name__ == '__main__' :
    test = C()

    test.x = 10 # setter

    print(test.x) # getter

    print(C.x.__doc__) # documentation

"""
10
Je suis la propriété 'x'.
"""

La seconde, à préférer, car plus simple, utilise la syntaxe des décorateurs. On remarque que la chaîne de documentation de la property est ici la docstring de la définition de la propriété x :

 
Sélectionnez
# !/usr/bin/python3
#-*- coding : utf-8 -*-
# fichier : property2.py

class C :
    def __init__(self) :
        self._x = None

    @property
    def x(self) :
        """Je suis la propriété 'x'."""
        return self._x

    @x.setter
    def x(self, value) :
        self._x = value

    @x.deleter
    def x(self) :
        del self._x

# autotest =========================================================
if __name__ == '__main__' :
    test = C()

    test.x = 10 # setter

    print(test.x) # getter

    print(C.x.__doc__) # documentation
"""
10
Je suis la propriété 'x'.
"""

9-2-2-c. Un autre exemple : la classe Cercle

Schéma de conception : nous allons tout d'abord définir une classe Point que nous utiliserons comme classe de base de la classe Cercle.

Image non disponible
Figure 9.2 - Conception UML de la classe Cercle

Voici le code de la classe Point :

 
Sélectionnez
class Point :
    def __init__(self, x=0, y=0) :
        self.x, self.y = x, y

    @property
    def distance_origine(self) :
        return math.hypot(self.x, self.y)

    def __eq__(self, other) :
        return self.x == other.x and self.y == other.y

    def __str__(self) :
        return "({0.x !s}, {0.y !s})".format(self)

L'utilisation de property permet un accès en lecture seule au résultat de la méthode distance_origine() considérée alors comme un simple attribut (car on l'utilise sans parenthèse) :

 
Sélectionnez
p1, p2 = Point(), Point(3, 4)
print(p1 == p2) # False
print(p2, p2.distance_origine) # (3, 4) 5.0

De nouveau, les méthodes renvoyant un simple flottant seront utilisées comme des attributs grâce à property :

 
Sélectionnez
class Cercle(Point) :
    def __init__(self, rayon, x=0, y=0) :
        super().__init__(x, y)
        self.rayon = rayon

    @property
    def aire(self) : return math.pi * (self.rayon ** 2)

    @property
    def circonference(self) : return 2 * math.pi * self.rayon

    @property
    def distance_bord_origine(self) :
        return abs(self.distance_origine - self.rayon)

Voici la syntaxe permettant d'utiliser la méthode rayon comme un attribut en lecture-écriture. Remarquez que la méthode rayon() retourne l'attribut protégé : __rayon qui sera modifié par le setter (la méthode modificatrice) :

 
Sélectionnez
@property
    def rayon(self) :
        return self.__rayon

    @rayon.setter
    def rayon(self, rayon) :
        assert rayon > 0, "rayon strictement positif"
        self.__rayon = rayon

Exemple d'utilisation des instances de Cercle :

 
Sélectionnez
def __eq__(self, other) :
        return (self.rayon == other.rayon
            and super().__eq__(other))

    def __str__(self) :
        return ("{0.__class__.__name__}({0.rayon !s}, {0.x !s},"
                "{0.y !s})".format(self))

if __name__ == "__main__" :
    c1 = Cercle(2, 3, 4)
    print(c1, c1.aire, c1.circonference)
    # Cercle(2, 3, 4) 12.5663706144 12.5663706144
    print(c1.distance_bord_origine, c1.rayon) # 3.0 2
    c1.rayon = 1 # modification du rayon
    print(c1.distance_bord_origine, c1.rayon) # 4.0 1

9-2-3. Le duck typing et les annotations

Il existe un style de programmation très pythonique appelé duck typing :

« S'il marche comme un canard et cancane comme un canard, alors c'est un canard ! »

Cela signifie que Python ne s'intéresse qu'au comportement des objets. Si des objets offrent la même API (interface de programmation), l'utilisateur peut employer les mêmes méthodes :

 
Sélectionnez
>>> class Person:
...     def help(self):
...         print('Heeeeeeelp!')
...
>>> class Duck:
...     def help(self):
...         print('Quaaaaaaaack!')
...
>>> class SomethingElse:
...     pass
...
>>> def inTheForest(x):
...     x.help()
...
>>> donald = Duck()
>>> john = Person()
>>> who = SomethingElse()
>>>
>>> for thing in [donald, john, who]:
...     try:
...         inTheForest(thing)
...     except AttributeError:
...         print("M'enfin ?")
Quaaaaaaaack!
Heeeeeeelp!
M'enfin ?

Un autre exemple :

 
Sélectionnez
>>> def calcule(a, b, c):
...     return (a + b) * c
...
>>> x = calcule(1, 2, 3)
>>> y = calcule('Hey-', 'Ho! ', 3)
>>> x
9
>>> y
'Hey-Ho! Hey-Ho! Hey-Ho! '
>>> z = calcule('a', 'b', 'c')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in calcule
TypeError: can't multiply sequence by non-int of type 'str'

Dans le dernier cas, on voit qu'il y a un problème quand on reçoit un type inattendu : le programme s'arrête en erreur à l'exécution. C'est bien là la différence avec les langages à typage statique où toute erreur de type est décelée dès la compilation.

Python est un langage très souple : la nature des données est dynamiquement découverte à l'exécution, ce qui offre plusieurs avantages :

  • on peut utiliser le « duck typing » : le code est court et clair ;
  • le langage est très facile à apprendre.

Mais, on l'a vu, des désagréments persistent… Les annotations ont été pensées pour corriger ce point.

 
Sélectionnez
# annotation simple et par défaut des paramètres :
def foo1(a : <expression>, b : <expression>=5) :
    …

# annotations pour *args et **kwargs :
def foo2(*args : <expression>, **kwargs : <expression>) :
    …

# annotation du type de retour :
def foo3() -> <expression> :
    …

Les annotations permettent de fournir des informations supplémentaires. Or, c'est important, ces informations optionnelles n'auront aucun impact sur l'exécution du code, mais des outils tierces parties pourront les utiliser pour par exemple :

  • faire de la vérification de type : on outrepasse alors le duck typing pour fixer un type précis, optimisé pour un usage particulier ;
  • fournir une aide aux éditeurs de codes ;
  • offrir un complément à la documentation des docstrings ;

Voici un exemple d'annotation :

 
Sélectionnez
>>> def ma_fonction(param1: 'Une annotation', param2: 2 + 3, param3: 'Autre annotation'='valeur_par_défaut'):
...     print(param1, param2, param3)
...
>>> ma_fonction(1, 2, 3) # aucun changement du comportement
1 2 3
>>> ma_fonction(1, 2)
1 2 valeur_par_défaut
>>> ma_fonction.__annotations__ # accès aux annotations
{'param3': 'Autre annotation', 'param2': 5, 'param1': 'Une annotation'}

9-3. Techniques fonctionnelles

9-3-1. Directive lambda

Issue de langages fonctionnels (comme Lisp), la directive lambda permet de définir un objet fonction anonyme dont le bloc d'instructions est limité à une expression dont l'évaluation fournit la valeur de retour de la fonction.

lambda [parameters] : expression

Par exemple cette fonction retourne s si son argument est différent de 1, une chaîne vide sinon :

 
Sélectionnez
>>> s = lambda x: "" if x == 1 else "s"
>>> s(3)
's'
>>> s(1)
''

Autres exemples illustrant les différences de syntaxe fonction/lambda :

 
Sélectionnez
>>> def f(x):
...     return x**2
...
>>> print(f(8))
64
>>>
>>> g = lambda x : x**2
>>> print(g(8))
64

9-3-2. Les fonctions map, filter et reduce

La programmation fonctionnelle est un paradigme de programmation qui considère le calcul en tant qu'évaluation de fonctions mathématiques. Elle souligne l'application des fonctions, contrairement au modèle de programmation impérative qui met en avant les changements d'état(38). Elle repose sur trois concepts : mapping, filtering et reducing qui sont implémentés en Python par trois fonctions : map(), filter() et reduce().

La fonction map()  :

map() applique une fonction à chaque élément d'une séquence et retourne un itérateur :

 
Sélectionnez
>>> map(lambda x:x, range(10))
<map object at 0x7f3a80104f50>
>>> list(map(lambda x:x, range(10)))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

On remarque que map() peut être remplacée par un générateur en compréhension.

La fonction filter()  :

filter() construit et renvoie un itérateur sur une liste qui contient tous les éléments de la séquence initiale répondant au critère : function(element) == True :

 
Sélectionnez
>>> list(filter(lambda x: x > 4, range(10)))
[5, 6, 7, 8, 9]

De même filter() peut être remplacée par un générateur en compréhension.

La fonction reduce()  :

reduce() est une fonction du module functools. Elle applique de façon cumulative une fonction de deux arguments aux éléments d'une séquence, de gauche à droite, de façon à réduire cette séquence à une seule valeur qu'elle renvoie :

 
Sélectionnez
>>> def somme(x, y):
...     print x, '+', y
...     return x + y
...
>>> reduce(somme, [1, 2, 3, 4])
1 + 2
3 + 3
6 + 4
10
>>> sum([1, 2, 3, 4])
10

La fonction reduce() peut être remplacée par une des fonctions suivantes : all(), any(), max(), min() ou sum().

9-3-3. Les applications partielles de fonctions

Issue de la programmation fonctionnelle, une PFA (application partielle de fonction) de n paramètres prend le premier argument comme paramètre fixe et retourne un objet fonction (ou instance) utilisant les n-1 arguments restants.

Les PFA sont utiles dans les fonctions de calcul comportant de nombreux paramètres. On peut en fixer certains et ne faire varier que ceux sur lesquels on veut agir :

 
Sélectionnez
>>> from functools import partial
>>> def f(m, c, d, u):
...     return 1000*m + 100*c + 10*d + u
...
>>> f(1, 2, 3, 4)
1234
>>> g = partial(f, 1, 2, 3)
>>> g(4)
1234
>>> h = partial(f, 1, 2)
>>> h(3, 4)
1234

Elles sont aussi utiles pour fournir des modèles partiels de widgets, qui ont souvent de nombreux paramètres. Dans l'exemple suivant, on redéfinit la classe Button en fixant certains de ses attributs (qui peuvent toujours être surchargés) :

 
Sélectionnez
from functools import partial
import tkinter as tk

root = tk.Tk()
# instanciation partielle de classe :
MonBouton = partial(tk.Button, root, fg='blue', bg='yellow')
MonBouton(text="Bouton 1").pack()
MonBouton(text="Bouton 2").pack()
MonBouton(text="QUITTER", bg='orange', fg='black', command=root.quit)
         .pack(fill=tk.X, expand=True)
root.title("PFA !")
root.mainloop()

Ce résultat est illustré Fig. 9.3.

Image non disponible
Figure 9.3 - PFA appliquée à un widget

9-4. La persistance et la sérialisation

La persistance consiste à sauvegarder des données afin qu'elles survivent à l'arrêt de l'application.

On peut distinguer deux étapes :

  • la sérialisation et la désérialisation ;
  • le stockage et l'accès.

La sérialisation est le processus de conversion d'un ensemble d'objets en un flux d'octets.

La désérialisation est le processus inverse qui recrée les données d'origine.

Le stockage utilise soit des fichiers, soit des bases de données.

Examinons des exemples simples.

9-4-1. Sérialisation avec pickle et json

9-4-1-a. Le module pickle

L'intérêt du module pickle est sa simplicité. Par contre, ce n'est pas un format utilisé dans d'autres langages, il n'est utile que tant que l'on reste dans le monde Python.

On peut utiliser une chaîne pour sérialiser, mais l'usage le plus commun est d'utiliser un fichier ouvert en mode binaire (contrairement au mode texte que l'on a déjà vu cf. § 4.7Fichiers textuels), avec le mode "wb". Par exemple pour un dictionnaire :

 
Sélectionnez
import pickle

favorite_color = {"lion": "jaune", "fourmi": "noire", "caméléon": "variable"}
# stocke ses données dans un fichier
pickle.dump(favorite_color, open("save.p", "wb"))

# retrouver ses données : pickle recrée un dictionnaire
dico = pickle.load(open("save.p", "rb"))
print(dico)

La lecture du fichier save.p produit :

 
Sélectionnez
{'fourmi': 'noire', 'lion': 'jaune', 'caméléon': 'variable'}

9-4-1-b. Le module json

Le module json permet d'encoder et de décoder des informations au format json(39). C'est un format d'échange très utile, implémenté dans un grand nombre de langages, plus lisible que XML, mais moins puissant.

On utilise la même syntaxe qu'avec pickle, à savoir dumps() et loads() pour une chaîne, dump() et load() pour un fichier, mais cette fois, un fichier textuel :

 
Sélectionnez
import json

# encodage dans un fichier
with open("json_tst", "w") as f :
    json.dump(['foo', {'bar' :('baz', None, 1.0, 2)}], f)

# décodage
with open("json_tst") as f :
    print(json.load(f))

La lecture du fichier json_tst produit :

 
Sélectionnez
['foo', {'bar': ['baz', None, 1.0, 2]}]

9-4-2. Stockage avec sqlite3

Le module sqlite3 est une bibliothèque écrite en C qui implémente une base de données relationnelle légère qui utilise des fichiers (ou même la mémoire).

Cette utilisation convient bien à de petits volumes de données et permet de maquetter le passage à des bases de données plus importantes.

Exemple de stockage d'une table :

 
Sélectionnez
import sqlite3

conn = sqlite3.connect('/home/bob/Tmp/tel_bd') # création du connecteur
with conn as c : # création du curseur
    # création de la table
    c.execute("""create table tel (nom text, prenom text, numero integer)""")
    # insertion d'une ligne de données
    c.execute("""insert into tel values ('Barthod', 'Stéphane', '86451')""")
    c.execute("""insert into tel values ('Bourrely', 'Jean', '86617')""")
    c.execute("""insert into tel values ('Brunel', 'Michaël', '86619')""")
    c.execute("""insert into tel values ('Canal', 'Bruno', '86604')""")
    c.execute("""insert into tel values ('Cordeau', 'Bob', '86624')""")
    c.execute("""insert into tel values ('Donath', 'Therèse', '86600')""")
    c.execute("""insert into tel values ('Marie', 'Florence', '86602')""")

Le fichier tel_bd produit peut être visualisé par le programme SQLite database browser (Fig. 9.4).

Image non disponible
Figure 9.4 - Visualisation d'un fichier de base de données sqlite3

9-5. Les tests

Dès lors qu'un programme dépasse le stade du petit script, le problème des erreurs et donc des tests se pose inévitablement(40).

Un test consiste à appeler la fonctionnalité spécifiée dans la documentation, avec un scénario qui correspond à un cas d'utilisation, et à vérifier que cette fonctionnalité se comporte comme prévu.

9-5-1. Tests unitaires et tests fonctionnels

On distingue deux familles de test :

tests unitaires : validations isolées du fonctionnement d'une classe, d'une méthode ou d'une fonction ;

tests fonctionnels : prennent l'application complète comme une boîte noire et la manipulent comme le ferait l'utilisateur final. Ces tests doivent passer par les mêmes interfaces que celles fournies aux utilisateurs, c'est pourquoi ils sont spécifiques à la nature de l'application et plus délicats à mettre en œuvre.

Dans cette introduction, nous nous limiterons à une courte présentation des tests unitaires.

9-5-2. Module unittest

Le module standard unittest fournit l'outil PyUnit, outil que l'on retrouve dans d'autres langages : JUnit (Java), NUnit (.Net), JSUnit (JavaScript), tous dérivés d'un outil initialement développé pour le langage SmallTalk : SUnit.

Par convention, chaque module est associé à un module de tests unitaires, placé dans un répertoire tests du paquet. Par exemple, un module nommé calculs.py aura un module de tests nommé tests/test_calculs.py.

PyUnit propose une classe de base, TestCase. Chaque méthode implémentée dans une classe dérivée de TestCase, et préfixée de test_, sera considérée comme un test unitaire(41) :

 
Sélectionnez
"""Module de calculs."""

# fonctions
def moyenne(*args) :
    """Renvoie la moyenne."""
    length = len(args)
    sum = 0
    for arg in args :
        sum += arg
    return sum/length

def division(a, b) :
    """Renvoie la division."""
    return a/b
 
Sélectionnez
"""Module de test du module de calculs."""

# import ----------------------------------------------------------------------
import sys
import unittest
from os.path import abspath, dirname
# on enrichit le path pour ajouter le répertoire absolu du source à tester :
sys.path.insert(0, dirname(dirname((abspath(__file__)))))
from calculs import moyenne, division

# définition de classe et de fonction -----------------------------------------
class CalculTest(unittest.TestCase) :
    def test_moyenne(self) :
        self.assertEquals(moyenne(1, 2, 3), 2)
        self.assertEquals(moyenne(2, 4, 6), 4)

    def test_division(self) :
        self.assertEquals(division(10, 5), 2)
        self.assertRaises(ZeroDivisionError, division, 10, 0)

def test_suite() :
    tests = [unittest.makeSuite(CalculTest)]
    return unittest.TestSuite(tests)

# autotest ===================================================================
if __name__ == '__main__' :
    unittest.main()

L'exécution du test produit :

 
Sélectionnez
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK

Pour effectuer une campagne de tests, il reste à créer un script qui :

  • recherche tous les modules de test : leurs noms commencent par test_ et ils sont contenus dans un répertoire tests ;
  • récupère la suite, renvoyée par la fonction globale test_suite ;
  • crée une suite de suites et lance la campagne.

9-6. La documentation des sources

Durant la vie d'un projet, on distingue plusieurs types de documentation :

  • les documents de spécification (ensemble explicite d'exigences à satisfaire) ;
  • les documents techniques attachés au code ;
  • les manuels d'utilisation et autres documents de haut niveau.

Les documents techniques évoluent au rythme du code et peuvent donc être traités comme lui : ils doivent pouvoir être lus et manipulés avec un simple éditeur de texte.

Il existe deux outils majeurs pour concevoir des documents pour les applications Python :

  • reStructuredText (ou reST) : un format enrichi ;
  • les doctests : compatibles avec le format reST. Ils permettent de combiner les textes applicatifs avec les tests.

9-6-1. Le format reST

Le format reStructuredText, communément appelé reST est un système de balises utilisé pour formater des textes.

À la différence de LaTeX ou d'HTML il enrichit le document de manière « non intrusive », c'est-à-dire que les fichiers restent directement lisibles.

9-6-1-a. docutils

Le projet docutils, qui inclut l'interpréteur reST, fournit un jeu d'utilitaires :

  • rst2html génère un rendu HTML avec une feuille de style css intégrée ;
  • rst2latex crée un fichier LaTeX équivalent ;
  • rst2s5 construit une présentation au format s5, qui permet de créer des présentations interactives en HTML.

9-6-1-b. Sphinx

Sphinx est un logiciel libre de type générateur de documentation. Il s'appuie sur des fichiers au format reStructuredText, qu'il convertit en HTML, PDF, man, et autres formats.

De nombreux projets utilisent Sphinx pour leur documentation officielle, cf. http://sphinx-doc.org/examples.html.

9-6-1-c. rst2pdf

Par ailleurs, le programme rst2pdf génère directement une documentation au format PDF.

Voici un exemple simple(42) de fichier texte au format reST.

On remarque entre autres que :

  • la principale balise est la ligne blanche qui sépare les différentes structures du texte ;
  • la structuration se fait en soulignant les titres des sections de différents niveaux avec des caractères de ponctuation (= - _ : , etc.). À chaque fois qu'il rencontre un texte ainsi souligné, l'interpréteur associe le caractère utilisé à un niveau de section ;
  • un titre est généralement souligné et surligné avec le même caractère, comme dans l'exemple suivant :
 
Sélectionnez
======================
Fichier au format reST
======================
Section 1
=========
On est dans la section 1.
Sous-section
~~~~~~~~~~~~
Ceci est une sous-section.
Sous-sous-section
.................
Ceci est une sous-sous-section.
.. et ceci un commentaire
Section 2
=========
La section 2 est ``beaucoup plus`` **intéressante** que la section 1.
Section 3
=========
La section 2 est un peu vantarde : la section 1 est *très bien*.
Une image au format "png"
~~~~~~~~~~~~~~~~~~~~~~~~~
.. figure:: helen.png
:scale: 30%
Image non disponible
Figure 9.5 - Exemple de sortie au format PDF

L'utilitaire rst2pdf, appliqué à ce fichier, produit le fichier de même nom (Fig. 9.5), mais avec l'extension .pdf.

9-6-2. Le module doctest

Le principe du literate programming (ou programmation littéraire) de Donald Knuth consiste à mêler dans le source le code et la documentation du programme.

Ce principe été repris en Python pour documenter les API via les chaînes de documentation (docstring). Des programmes comme Epydoc peuvent alors les extraire des modules pour composer une documentation séparée.

Il est possible d'aller plus loin et d'inclure dans les chaînes de documentation des exemples d'utilisation, écrits sous la forme de session interactive.

Examinons deux exemples.

Pour chacun, nous donnerons d'une part le source muni de sa chaîne de documentation dans lequel le module standard doctest permet d'extraire, puis de lancer ces sessions pour vérifier qu'elles fonctionnent et, d'autre part un résultat de l'exécution.

9-6-2-a. Premier exemple : documentation1.py

 
Sélectionnez
# -*- coding : utf-8 -*-
"""Module d'essai de doctest."""

import doctest

def somme(a, b) :
    """Renvoie a + b.

    >>> somme(2, 2)
    4
    >>> somme(2, 4)
    6
    """
    return a+b

if __name__ == '__main__' :
    print("{ :-^40}".format(" Mode silencieux "))
    doctest.testmod()
    print("Si tout va bien, on n'a rien vu !")

    print("\n{ :-^40}".format(" Mode détaillé "))
    doctest.testmod(verbose=True)

L'exécution de ce fichier donne :

 
Sélectionnez
----------- Mode silencieux ------------
Si tout va bien, on n'a rien vu !

----------- Mode détaillé ------------
Trying:
    somme(2, 2)
Expecting:
    4
ok
Trying:
    somme(2, 4)
Expecting:
    6
ok
1 items had no tests:
    __main__
1 items passed all tests:
    2 tests in __main__.somme
2 tests in 2 items.
2 passed and 0 failed.
Test passed.

9-6-2-b. Deuxième exemple : documentation2.py

 
Sélectionnez
# -*- coding : UTF-8 -*-
"""Module d'essai de doctest."""

# fonctions
def accentEtrange(texte) :
    """Ajoute un accent étrange à un texte.

    Les 'r' sont Triplés, les 'e' suivi d'un 'u'

    Exemple :

    >>> texte = "Est-ce que tu as regardé la télé hier soir ? Il y avait un théma sur les ramasseurs d'escargots en Laponie, ils en bavent..."
    >>> accentEtrange(texte)
    Est-ceu queu tu as rRreugarRrdé la télé hieurRr soirRr ? Il y avait un théma surRr leus rRramasseuurRrs d'euscarRrgots eun Laponieu, ils eun baveunt...
    
    Cette technique permet d'internationaliser les applications
    pour les rendre compatibles avec certaines régions françaises.
    """
    texte = texte.replace('r', 'rRr')
    print(texte.replace('e', 'eu'))

def _test() :
    import doctest
    doctest.testmod(verbose=True)

if __name__ == '__main__' :
    _test()

L'exécution de ce fichier donne :

 
Sélectionnez
Trying:
    texte = "Est-ce que tu as regardé la télé hier soir ? Il y avait un théma sur les ramasseurs d'escargots en Laponie, ils en bavent..."
Expecting nothing
ok
Trying:
    accentEtrange(texte)
Expecting:
    Est-ceu queu tu as rRreugarRrdé la télé hieurRr soirRr ? Il y avait un théma surRr leus rRramasseuurRrs d'euscarRrgots eun Laponieu, ils eun baveunt...
ok
2 items had no tests:
    __main__
    __main__._test
1 items passed all tests:
    2 tests in __main__.accentEtrange
2 tests in 3 items.
2 passed and 0 failed.
Test passed.

9-6-3. Le développement dirigé par la documentation

Comme on peut le voir, la documentation intégrée présente néanmoins un défaut : quand la documentation augmente, on ne voit plus le code !

La solution est de déporter cette documentation : la fonction doctest.testfile() permet d'indiquer le nom du fichier de documentation.

Qui plus est, on peut écrire ce fichier au format reST, ce qui permet de faire coup double. D'une part, on dispose des tests intégrés à la fonction (ou à la méthode) et, d'autre part, le même fichier fournit une documentation à jour.

9-6-3-a. Exemple : test_documentation2.py

Fichier de documentation :

 
Sélectionnez
Le module ``accent``
====================

Test de la fonction ``accentEtrange``
-------------------------------------

Ce module fournit une fonction ``accentEtrange``.
On peut ainsi ajouter un accent à un texte :

        >>> from doctest2 import accentEtrange
        >>> texte = "Est-ce que tu as regardé la télé hier soir ? Il y avait un théma sur
        les ramasseurs d'escargots en Laponie, ils en bavent..."
        >>> accentEtrange(texte)
        Est-ceu queu tu as rRreugarRrdé la télé hieurRr soirRr ? Il y avait un théma surRr
        leus rRramasseuurRrs d'euscarRrgots eun Laponieu, ils eun baveunt...

Les ``r`` sont triplés et les ``e`` épaulés par des ``u``. Cette technique permet
de se passer de systèmes de traductions complexes pour faire fonctionner
les logiciels dans certaines régions.

Source du module :

 
Sélectionnez
import doctest
doctest.testfile("test_documentation2.txt", verbose=True)

Nous produisons la documentation HTML par la commande :

 
Sélectionnez
rst2html test_documentation2.txt test_documentation2.html

Elle est illustrée Fig. 9.6.

Image non disponible
Figure 9.6 - Documentation au format HTML du script test_documentation2.py

précédentsommairesuivant
Voire incontournable si on en croit la loi de Murphy
Célèbre utilitaire de recherche d'un motif dans le monde Unix.
callable en anglais.
Cette notion sera développée ultérieurement (cf. §9.3.1Directive lambda).
Slogan des développeurs Python : « We're all consenting adults here » (nous sommes entre adultes consentants).
Le name mangling.
Cf. Wikipedia.
JavaScript Object Notation.
Cf. la loi de Murphy.
Cf. [7].
Cf. [7].

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2015 Kordeo. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.