Blog ENI : Toute la veille numérique !
🐠 -25€ dès 75€ 
+ 7 jours d'accès à la Bibliothèque Numérique ENI. Cliquez ici
Accès illimité 24h/24 à tous nos livres & vidéos ! 
Découvrez la Bibliothèque Numérique ENI. Cliquez ici
  1. Livres et vidéos
  2. Scripting Python sous Linux
  3. Aller plus loin avec le langage Python et la POO
Extrait - Scripting Python sous Linux Développez vos outils système (2e édition)
Extraits du livre
Scripting Python sous Linux Développez vos outils système (2e édition)
2 avis
Revenir à la page d'achat du livre

Aller plus loin avec le langage Python et la POO

Introduction

Après la découverte du langage, de son côté objet, de quelques modules et librairies, il reste encore quelques notions à aborder dans la POO, dans le langage Python objet, et c’est le but de ce chapitre.

Quelques concepts objet essentiels

Cette section ne couvre que les cas les plus courants. Mais il faut savoir qu’il existe 23 patrons de conceptions en POO (Design pattern en anglais). Tous sont décrits dans le livre Design Patterns: Elements of Reusable Software paru en 1994 et écrit par "la bande des quatre" (Gang of Four : Gof) : messieurs Gamma, Helm, Johnson et Vlissides.

1. Le polymorphisme

Juste avant d’aborder l’héritage multiple, il faut d’abord connaître le concept de polymorphisme en POO.

Il en existe plusieurs sortes, mais, pour une fonction en Python, le polymorphisme signifie qu’une même fonction peut être utilisée pour différents types.

Comme la fonction len(), par exemple, qui permet de connaître le nombre d’éléments d’un objet.

>>> len("abcd")          # une chaîne de caractères 
4 
>>> len( [1,2,3,4] )     # une liste 
4 

La fonction len() est dite polymorphe car elle fonctionne avec plusieurs types de données.

Dans le cas de Python, il s’agit du concept de "duck typing" (typage canard).

Duck typing est un concept issu de la phrase attribuée à James Withcomb Riley (écrivain et poète américain du début du XXe siècle) : "Si je vois un oiseau qui vole comme un canard, cancane comme un canard et nage comme un canard, alors j’appelle cet oiseau un canard".

Grossièrement, dans le cas de notre fonction len(), si un objet a une fonction permettant de le dénombrer, alors c’est qu’il est dénombrable.

Et en Python, dénombrer un objet, c’est lui demander de lancer une fonction spéciale __len__ (les fonctions spéciales en Python seront bientôt abordées).

>>> "abcd".__len__() 
4 

Si un objet possède cette fonction, alors il est dénombrable… « si ça vole comme un canard et si ça cancane comme un canard… »

En Python, on tient compte des caractéristiques des objets plutôt que du type des objets.

2. L’héritage multiple

Dans le chapitre de découverte de la POO, la notion d’héritage a été...

Les méthodes spéciales d’instances

Nous venons de le voir, les instances d’objets Python ont des méthodes spéciales. Quand on veut compter les éléments d’un objet, on utilise l’instruction len().

Celle-ci exécute la fonction __len__ de l’objet. Si elle n’existe pas, soit c’est un oubli du programmeur, soit la fonction len() n’est pas applicable.

Les doubles underscores signifient (par convention) qu’il s’agit d’une fonction privée.

Elles n’ont de spécial que le nom, car en fait il s’agit de fonctions définies par le langage pour être utilisées dans certains contextes.

Par exemple :

  • Le contexte de dénombrement avec __len__

  • Le contexte de conversion en chaîne de caractères avec __str__

  • Le contexte d’initialisation d’instance avec __init__

Un petit exemple avec __len__ :

>>> class A(): 
...   def __len__(self): 
...     return 5 
... 
>>> o = A() 
>>> len(o) 
5 

Il y en a beaucoup d’autres et cette section va détailler les principales.

1. Les fonctions spéciales classiques

Voici un tableau des fonctions spéciales les plus courantes ; certaines sont connues depuis le début.

La plupart nécessitent juste d’être implémentées aussi simplement que __len__, d’autres, par contre, nécessitent d’être étudiées auparavant.

Fonction

Contexte

Remarque sur l’utilisation

__del__

Destruction de l’objet

 

__init__

Initialisation de l’objet

 

__new__

Appelé avant la création de l’objet

Doit retourner un nouvel objet

__repr__

Représentation de l’objet

print ou dans l’interpréteur

__getattr__

Accès aux attributs inexistants

gettatr(objet, "nom")

__getattribute__

Accès aux attributs

Tous les attributs

__setattr__

Accès aux attributs

settatr(objet, "nom")

__delattr__

Suppression d’un attribut

 

__getitem__

objet[index]

 

__setitem__

objet[index] = valeur

 

__delitem__

del objet[index]

 

__contains__

objet ’in’ objet

 

__len__

Dénombrement, longueur de l’objet

 

__call__

Rendre un objet appelable (callable)

 

__missing__

Clé...

Le gestionnaire de contexte (with) __enter__, __exit__

Les fonctions spéciales permettent de faire beaucoup de choses très sympathiques, mais avec celles-ci,  __enter__ et __exit__, Python nous amène encore plus loin.

Ces deux fonctions permettent de se créer des classes utilisables avec le mot-clé ’with’, qui est ce que l’on nomme un gestionnaire de contexte.

L’instruction with associée à un bloc de code permet de créer un contexte particulier valable pour tout le bloc de code.

L’exemple typique d’utilisation est celle de l’ouverture d’un fichier.

# exemple avec l'ouverture d'un fichier (cas typique) 
with open(fichier) as f: 
   <bloc de code> 
 
# à la fin du bloc de code le fichier 
# sera fermé et la variable 'f' qui représente le fichier 
# sera éliminée. 

Cela existe déjà, mais si l’on voulait écrire la même chose, il faudrait implémenter la fonction __enter__ et ouvrir le fichier, que l’on fermerait dans la fonction __exit__.

En comprenant comment fonctionnent ces deux fonctions spéciales, l’une à l’entrée d’un contexte et l’autre en sortie, tout de suite on pense à la mesure du temps que prend un bloc de code.

D’où l’idée de cette classe...

Les objets mutables et non mutables

Autant le dire tout de suite, cette section ne sera certainement pas votre section préférée. Mais comme il s’agit d’une notion importante, il faut l’aborder.

Dans le langage Python, tout est objet, et il existe des objets mutables et des objets non mutables.

Par mutable, il faut entendre modifiable, et surtout bien faire la différence entre modification et assignation.

Une variable est une référence sur un objet.

Quand on assigne une variable, on ne change pas la valeur de l’objet mais on change d’objet.

Quand on modifie un objet, on modifie le contenu de l’objet.

Quand on écrit :

a = 1 
a = 2 

a référence l’objet 1 (l’entier 1), puis référence l’objet 2 (l’entier 2), ce n’est pas l’objet 1 qui a changé, c’est la valeur de ’a’.

Et il est facile de comprendre que l’on ne peut pas modifier l’objet ’1’ (l’entier 1).

Par contre, si on écrit :

>>> a = [1]          # création d'une liste avec l'objet 1 
>>> a[0] = 2         # modification de la liste 
>>> a 
[2] 

on modifie l’objet liste créé, l’objet liste est donc modifiable.

D’où la notion importante des objets mutables et des objets non mutables.

Ne partez pas tout de suite, vous allez bientôt comprendre ce à quoi cela sert.

1. Les mutables

Les listes, les dictionnaires et les sets sont des objets mutables et peuvent donc être modifiés. D’ailleurs, ils ont des méthodes prévues à cet effet.

>>> a = [1,2,3] 
>>> id(a) 
140647764070880      # Id de a 
 
>>> a.append(4) 
>>> a 
[1, 2, 3, 4] 
 
>>> id(a) 
140647764070880      # C'est le même objet...

Quelques informations supplémentaires sur les classes en Python

1. Les attributs implicites

Une classe, même vide de tout attribut et méthode, contient pour sa gestion interne des attributs implicites.

Et comme toujours avec Python, rien n’est caché, il suffit juste de savoir où cela se trouve.

Soit le script suivant :

class A(): 
   pass 
 
for i in dir(A): 
   print("%20s |" % i, "%s" % getattr(A,i))  

qui vous donnera tout ce qui se trouve dans une classe.

__class__        |  <class 'type'> 
__delattr__      |  <slot wrapper '__delattr__'  
                          of 'object' objects> 
__dict__         |  {'__module__': '__main__',  
                  '__dict__': <attribute '__dict__'  
                          of 'A' objects>,  
                  '__weakref__': <attribute '__weakref__'  
                          of 'A' objects>, '__doc__': None} 
__dir__          |  <method '__dir__' of 'object' objects> 
__doc__          |  None 
__eq__           |  <slot wrapper '__eq__' of 'object' objects> 
__format__       |  <method '__format__' of 'object' objects> 
__ge__           |  <slot wrapper '__ge__' of 'object' objects> 
__getattribute__ |  <slot wrapper '__getattribute__'  
                          of 'object' objects> 
__gt__           |  <slot wrapper '__gt__' of 'object' objects> 
__hash__         |  <slot wrapper '__hash__' of 'object'...

Les docstrings - chaînes de documentation

1. Définition

La docstring, chaîne de caractères de documentation, permet de documenter son code. Pas besoin de l’assigner, il suffit de placer les explications au bon endroit, en règle générale, au début de l’objet que l’on veut documenter.

C’est une habitude à prendre, surtout si vous devez travailler à plusieurs sur un projet.

Écrire des docstrings offre de nombreux avantages.

  • Il existe une fonction help() dans l’interpréteur qui utilise les docstrings.

  • La plupart des outils avec une option "python" affichent cette documentation.

  • Des outils permettent de générer une doc en HTML à partir des docstrings.

  • C’est un mécanisme standardisé de documentation dans le monde Python.

  • Le code Python peut utiliser la docstring pour la lire ou l’afficher.

  • On peut mettre des tests dans les docstrings, qui servent alors d’exemples d’utilisation.

  • Dans un fichier, on a le code, la documentation et les tests.

  • Deux niveaux simples et avancés.

2. Usage

Voici un exemple rapide :

>>> def f(arg1, arg2): 
...   """ 
...   Documentation de la fonction f() 
...   """ 
...   pass 
... 
>>> help(f) 
python docstring 
Help on function f in module __main__: 
 
f(arg1, arg2) 
   Documentation de la fonction f() 
(END) 

Rien de bien compliqué, il suffit de documenter en utilisant des triples quotes.

Il est possible d’insérer des docstrings en début de script ou de module, en début de classe ou de fonction.

Voici un exemple un peu plus complet :

# fichier : classe/docstr1.py 
 
# ----------------------- 
# Un module documenté 
# ----------------------- ...

Les décorateurs

Python offre beaucoup de fonctionnalités, des modules, des fonctions, des classes, et cet ouvrage permet de survoler, espérons-le, l’essentiel.

Avec les décorateurs, on aborde des notions avancées de programmation, mais comme c’est très pratique et couramment utilisé, il faut savoir au moins de quoi il s’agit.

Un décorateur en Python, ce n’est ni plus ni moins qu’une fonction qui va s’exécuter avant une fonction.

Cela permet, entre autres, de modifier le comportement de cette fonction avant qu’elle ne soit lancée.

La syntaxe est la suivante :

@décorateur 
def la_fonction_a_décorer() : 
      pass 

Mais attention, ce n’est pas aussi intuitif que cela en a l’air.

Premier piège : le décorateur s’exécute à la définition de la fonction ; on lui donne une fonction et il doit retourner une fonction, qui viendra en remplacement de la fonction "décorée".

Exemple de décorateur qui ne fait rien

#fichier : deco/deco0.py 
 
def mon_deco(fonction): 
   return fonction 
 
@mon_deco 
def fonction1(): 
    print("Je suis la fonction 1 ") 
 
@mon_deco 
def fonction2(): 
    print("Je suis la fonction 2 ") 
 
@mon_deco ...

Les dataclasses

KISS, acronyme pour Keep It Simple Stupid (ne complique pas les choses) est un principe que l’on retrouve dans la philosophie d’Unix et dans le « zen de Python ».

Plus d’informations sur le Principe KISS : https://fr.wikipedia.org/wiki/Principe_KISS

Python est un langage fondamentalement simple, et pourtant, certains sont allés plus loin pour simplifier encore la définition d’une classe d’objet, et le résultat donne les dataclasses.

Une classe en Python dans sa forme la plus simple est :

>>> class Simple():  
...     pass  
...  
>>>  
>>> a = Simple()        # Création d'un instance   
>>> dir(a)  
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__',  '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__',  '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__',  '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__',  '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']  
>>>...

La structure conditionnelle match...case et les classes

Voici enfin la deuxième partie de l’instruction vedette des dernières versions de Python.

Précédemment, cette instruction a été présentée dans son utilisation avec les littéraux et les types de données Python dans le chaptire Le langage Python.

Mais qu’en est-il avec les classes et les objets ?

Tout d’abord une classe n’est, ni plus ni moins, qu’un type Python créé par le développeur, un type « utilisateur ».

Un objet et ses attributs sont utilisables comme des littéraux, ou une classe avec ses variables de classe.

Exemple

#fichier: http_error.py  
import requests  
  
class HTTP_ERROR():  
   OK = 200  
   PAS_TROUVE = 404  
   ERR_SERVER = 500  
  
r = requests.get("http://example.com/page_inexistante.html")  
  
match r.status_code:  
  
   case HTTP_ERROR.OK:  
       print(" Bizarre la page existe ...")  
   case HTTP_ERROR.PAS_TROUVE:  
       print(" C'est normal que la page n'existe pas ")  
   case HTTP_ERROR.ERR_SERVER:  
       print(" Il y a une erreur ") 

D’ailleurs, dans un cas comme celui-ci, il vaut mieux utiliser les classes Enum ou OrderedEnum qui amènent beaucoup plus de fonctionnalités.

Mais les développeurs du langage Python ne se sont pas arrêtés là, et on peut dire qu’ils sont allés suffisamment loin pour que cette structure change la manière de coder en Python.

Petit rappel : match...case fonctionne avec un sujet et des motifs, le sujet est évalué et si un motif correspond alors son bloc de code est exécuté.

Et voici en français ce qui suscite tellement d’enthousiasme.

Il est possible d’avoir un motif pour un objet mais aussi pour une valeur d’un de ses attributs.

Au premier abord, dit comme ça, cela ne semble pas extraordinaire mais voici un exemple plus parlant bien que non fonctionnel.

Le sujet (match) pourrait être un objet « évènement »...

Les itérateurs, générateurs et autres expressions génératrices

Avec le langage Python, il est possible de créer tous les objets informatiques imaginables, et même plus. Dans le "plus", on trouve, entre autres, les notions d’itérateurs et de générateurs.

1. Les itérateurs

Un itérateur est un objet qui permet de parcourir tous les objets contenus dans un autre objet. Il est possible avec Python de se créer ses propres itérateurs.

Mais avant, il faut étudier un peu ce qu’il se passe dans un exemple des plus classiques, la boucle ’for’ :

for <variable> in <object itérable>: 
   <bloc de code> 

Comment fonctionne ce pilier de la programmation ?

En Python, schématiquement, cela se passe de cette manière :

Tout d’abord, l’interpréteur commence par invoquer la fonction iter(<object_itérable>) pour obtenir en retour un objet itérable.

Une fois l’objet capté, la boucle ’for’ va chercher à obtenir la prochaine valeur en appelant la fonction next() sur l’objet et affecter cette valeur à la variable prévue à cet effet.

La boucle appellera la fonction next() jusqu’à ce qu’il n’y ait plus de valeur et que la fonction next() renvoie une exception.

Pour bien comprendre comment cela fonctionne, il est possible de le faire manuellement. 

>>> objet_itérable = iter([1,2,3]) 
>>> objet_itérable 
<list_iterator object at 0x7fda31b8c870> 
 
>>> next(objet_itérable) 
1 
>>> next(objet_itérable) 
2 
>>> next(objet_itérable) 
3 
 
>>> next(objet_itérable) 
Traceback (most recent call last): 
 File "<stdin>", line 1, in <module> 
StopIteration 
 
>>> dir(objet_itérable) 
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__',  
'__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', 
'__init__', '__init_subclass__'...

Gérer ses propres exceptions

C’est un fait, tout est objet avec Python, et bien entendu les exceptions n’échappent pas à la règle.

Cela peut ne jamais arriver, surtout avec de petits scripts, mais sur de gros projets, les exceptions sont une manière de communiquer avec les autres codeurs et les utilisateurs.

Après tout, on entend parfois que l’on peut juger de la qualité d’un produit informatique grâce au nombre de messages d’erreurs qu’il est capable de gérer.

En Python, les exceptions proviennent de la classe… Exception.

Et pour créer vos propres exceptions, il suffit de créer une classe qui hérite de la classe Exception et de redéfinir la fonction __str__.

Voici un script d’exemple :

# fichier : except/except4.py 
 
class MonException(Exception): 
   """ 
   La documentation est primordiale surtout pour les exceptions 
   qui, je le rappelle, ne sont pas des objets anodins 
   """ 
   def __init__(self, message): 
       self.message = message 
 
   def __str__ (self): 
       return "MonException msg=%s" % self.message 
 
 
try: 
   print("Avant l'erreur") 
   raise MonException("MESSAGE...

Les fonctions natives

Python possède quelques fonctions natives utilisables directement. En voici la liste, classée par thématique.

Pour une liste alphabétique, voir la documentation officielle : https://docs.python.org/fr/3/library/functions.html

1. Les fonctions natives inclassables

input([prompt])

Lit sur l’entrée standard.

Si le module readline est chargé , input() s’en servira pour gérer un historique.

open()

Ouvre un fichier (cf. chapitre Le langage Python - Les entrées/sorties (fichier et autres)).

print()

La fonction d’impression (cf. chapitre Le langage Python - La fonction print()).

breakpoint()

Utilisé avec le débogueur, pose un point d’arrêt pour stopper l’exécution.

compile(code, filename, mode)

Compile du code source Python.

S’utilise de la manière suivante :

>>> code = compile("2+2", "test", "single") 
>>> exec(code) 
4 

mode peut être :

  • eval : évaluation d’une expression.

  • exec : le source peut être des blocs de code complets.

  • single : le source est composé d’une instruction interactive comme dans l’interpréteur.

eval(expression, [globals[, locals]])

Évalue une expression.

>>> eval("3+2") 
5 

exec()

Exécute du code Python compilé avec compile().

help()

Appelle le système d’aide natif de Python. 

format(valeur[, format])

Formate une valeur (cf. chapitre Le langage Python - La fonction print() - Print() formatage chaîne.format()).

globals()

Retourne la table des variables globales (cf. chapitre Le langage Python - Les fonctions - Les fonctions et la portée des variables).

locals()

Retourne la table des variables locales (cf. chapitre Le langage Python - Les fonctions - Les fonctions et la portée des variables).

2. Les fonctions natives binaires

Les fonctions natives binaires permettent la manipulation d’octets et de tableaux d’octets.

bytearray()

Crée un tableau d’octets.

bytes()

Renvoie un nouvel objet bytes.

memoryview()

Permet de manipuler un objet comme une suite d’octets sans effectuer de copie.

Plus de détails sur : https://docs.python.org/fr/3/library/stdtypes.html#bytes-methods

3. Les fonctions natives de conversion ou de création...

Résumé

À ce stade, il est temps de passer au grade supérieur et de ne plus se considérer comme une "ceinture blanche" en langage Python. Les types de données, le langage, la POO, la librairie standard... tout cela a été abordé. Il faut maintenant pratiquer, le plus régulièrement possible, le langage Python.

Les cinq chapitres suivants abordent des sujets beaucoup plus pratiques, avec notamment ce que Python peut apporter comme informations sur un système, la manipulation de différents formats de fichiers ou la simulation d’activité sur une base de données, en passant par la génération de rapport en PDF.