pour réutiliser du code en python¶
DRY = don’t repeat yourself : cut’n paste is evil
| fonctions | pas d’état après exécution | 
| modules | garde l’état, une seule instance par programme | 
| classes | instances multiples, chacune garde l’état, héritage | 
à quoi sert un module ?¶
pour réutiliser du code, qui peut venir:
- de la librairie standard, e.g. - import math
- d’un package installé depuis - pypi.orgavec- pip install numpy- voir PyPI - the Python Package Index - https://pypi.org/ 
 
- d’un fichier / dossier de votre propre application 
 de cette façon on peut couper son code en morceaux plus digestes
c’est quoi un module ?¶
- un module est un objet Python, correspondant au chargement en mémoire 
 du code venant d’un fichier ou dossier source
 (dans le cas d’un dossier on parle alors d’un package - on en reparlera)
- les différents composants du code sont alors accessibles comme un attribut du module 
 (c’est à dire les variables qui sont définis au toplevel dans le fichier source)
regardons ce qu’il y a dans ce fichier mod.py
1 2 3 4 5 6 7 8 9 10# comme la variable GLOBALE est définie au niveau le plus haut # dans le fichier, elle va être accessible comme un attribut # dans le module mod, c'est à dire mod.GLOBALE GLOBALE = 100 # pareil ici, la fonction spam nous sera accessible comme mod.spam def spam(data): print(f"dans mod.spam, data={data}")
# pour utiliser ce code, je commence par l'importer
import mod# ce qui a pour effet de définir la variable 'mod'
# qui désigne - en Python tout est objet -
# un objet qui est de type .. wait for it ..
mod<module 'mod' from '/home/runner/work/slides/slides/notebooks/mod.py'># et depuis cet objet là, je peux accéder via ses attributs
# aux différents morceaux du code, par exemple
mod.GLOBALE100# ou encore 
mod.spam('good')dans mod.spam, data=good
rappel: la notion d’attribut¶
on l’a déjà vu, mais pour rappel
- un attribut est une annotation sur un objet (ici le module - mod)
- dans l’espace de noms de l’objet, on “attache” un nom (ici - spam) qui désigne autre objet (ici la fonction)
- pour utiliser (aller chercher) un attribut, la syntaxe est - obj.attribute
- un attribut n’est pas une variable - les variables sont résolues par liaison lexicale 
- les attributs sont résolus à run-time 
 
plusieurs formes d’import¶
la forme import mod est la plus basique; il en existe des variantes:
| code | définit la variable | commentaire | 
| 
 | 
 | de type module, avec des attributs | 
| 
 | 
 | à part ça, même effet que  | 
| 
 | 
 | on n’a pas accès au module, seulement un attribut | 
package = module pour un dossier¶
il est possible d’organiser un gros code source dans un dossier, qui peut à son tour contenir d’autres dossiers...
du coup, le package est simplement un module, mais qui correspond à un dossier
dans ce cas-là, les attributs du module/package vont nous permettre de nous y retrouver:
si on a cette arborescence de fichiers
pack1/
    pack2/
        mod.py
            class Fooon retrouve l’équivalent dans l’espace des modules
pack1
pack1.pack2
pack1.pack2.mod
pack1.pack2.mod.Fooimporter un morceau du package¶
import dir.dir2.modulename
dans ce genre de contexte on peut avoir envie de ne charger qu’une partie du package, c’est possible avec quasiment la même syntaxe:
voyons le contenu de cet autre module qui s’appelle aussi mod
1 2 3 4FOO = "module with the same name" def eggs(): print(f"eggs from pack1.pack2.mod")
# on pourrait l'importer comme ceci
# remarquez que l'on charge bien les packages intermédiaires
# qui sont pack1 et pack1.pack2
import pack1.pack2.modpack1 init
pack2 init
# et l'utiliser comme cela
pack1.pack2.mod.FOO'module with the same name'les attributs du package¶
voici la réponse à la question de tout à l’heure:
# si on fait abstraction des attributs spéciaux, 
# notre package a les attributs suivants
[att for att in vars(pack1) if '__' not in att]['x', 'pack2', 'FOO']on y trouve
- pack2qui est là à cause du sous-dossier- pack2
- xqui est initialisé à 1 dans- pack2/__init__.py
- et - FOOqui résulte de l’import, et qui est aussi une variable globale dans- pack2/__init__.py
que fait une importation ?¶
principalement deux choses:
- vérifier si le module est déjà chargé, et sinon: 
- trouver le fichier/dossier correspondant au module on en reparle un peu plus tard (c’est un sujet délicat) rappel: on ne met pas le - .pydu fichier lors d’un import
- compiler (si besoin) le module en byte-code 
 cela est caché dans les dossiers- __pycache__pas besoin de s’en occuper, on n’en parlera plus
- charger en mémoire le module pour construire les objets qu’il définit 
 typiquement fonctions, classes, variables globales au module
 et les ranger dans les attributs du module
- affecter la variable locale - comme on l’a vu plus haut 
localisation du fichier du module¶
il est temps d’aborder le point le plus délicat avec les modules: comment fait le langage pour trouver le fichier (ou, à nouveau, le dossier) lorsqu’on importe un module ?
c’est un sujet souvent mal compris, c’est pourquoi on va essayer de bien décortiquer...
objectifs¶
avant de vous donner l’algorithme utilisé, souvenons-nous bien de l’objectif
il s’agit de pouvoir importer aussi bien:
- la librairie standard 
- les librairies installées avec pip 
- les morceaux de code qui viennent avec votre propre application 
l’algorithme¶
du coup on a choisi de chercher les fichiers dans l’ordre suivant
- d’abord on cherche dans le dossier où se trouve le point d’entrée du programme 
- ensuite, si elle est définie, dans le ou les dossiers configurés dans la variable d’environnement - PYTHONPATH
- enfin dans les répertoires des librairies standards, ceci est configuré à l’installation 
 c’est dans ces endroits-là qu’on trouve la lib. standard, et les libs installées avec- pip
vous savez que pour lancer un programme Python, on exécute en fait quelque chose comme
python mon-application.pyeh bien ce fichier mon-application.py c’est ce qu’on appelle le point d’entrée
pour ceux qui ont fait du C/C++ ça correspond au main()
les fausses bonnes idées¶
pour commencer, je vous recommande fortement de ne pas utiliser PYTHONPATH dans la vie de tous les jours, c’est vraiment réservé à des usages très spécifiques, notamment lorsqu’on déploie des solutions hostées à la jupyterhub ou autres
pour être tout à fait complet, il faut savoir que cette liste est en fait calculée au démarrage de l’interpréteur, et rangée dans la variable sys.path ... que vous pouvez tout à fait modifier par programme pour ajouter d’autres endroits
toutefois cette pratique n’est, ici encore, pas recommandée en dehors de quelques cas d’usage très spécifiques
du monolithe au package¶
pour vos premiers pas, vous allez en général compliquer progressivement:
- en premier, on commence par écrire tout dans un seul fichier; là bien sûr, zéro souci 
- dès que ça grossit un peu, on va vouloir couper en plusieurs fichiers et dans ce cas là, il suffit de les mettre tous dans un même dossier, y compris le point d’entrée donc, et grâce à la règle numéro 1, on va trouver facilement nos propres modules 
toutefois, cette approche atteint rapidement sa limite, car
- pour lancer le programme, on doit, soit se placer dans le bon dossier avant de faire - python mon-application.py
 soit donner à python le chemin complet- bash le/chemin/ou/se/trouve/mon-application.py
 et ce n’est pas forcément très pratique
- ça oblige à éviter des noms déjà pris; si un de vos fichiers s’appelle - turtle.py, vous ne pourrez plus utiliser le module- turtlede la libraries standard ! toujours à cause de la règle 1
- si on veut écrire des tests, il faut les placer dans le même dossier aussi... 
bref, si le projet a un peu de substance on est vite amené à vouloir organiser ses sources un peu mieux; pour cela la solution est de créer un package pour y mettre votre code, sauf que c’est moins trivial qu’on pourrait le penser ou en tous cas le souhaiter, et c’est précisément le sujet du notebook suivant :)
en première lecture vous pouvez vous arrêter ici, la suite couvre des aspects plutôt avancés de l’importation
notions avancées¶
références partagées¶
- les instructions - importet- fromsont des affectations implicites de variables- on a donc le problème des références partagées sur des mutables 
 
voici un exemple (ne pas faire ça dans du vrai code !)
# ne surtout pas faire un truc comme ça...
# car ça modifie le contenu du module
# et donc ça impacte tout le programme !
import math
math.pi = 10.en fait je viens de modifier math.pi pour tout mon programme !!
on n’aurait pas le problème avec from parce que là, ça crée une variable locale
# les autres modules ne sont pas impactés
from math import pi
pi = 10exécuter un module comme un script¶
un module peut avoir deux rôles
- un module classique qui doit être importé - import module
- un script exécutable - $ python module.py
chaque module a un attribut __name__ qui est défini par l’import
1 2 3 4eggs = 1 def spam(s): print(s, 'spam')
import toplevel
print(toplevel.__name__)toplevel
l’idiome if __name__ == "__main__"¶
sauf que, si le module est le point d’entrée (on l’a lancé avec python foo.py), alors son exécution n’est pas le résultat d’un import
... et du coup dans ce cas-là on ne trouve pas dans __name__ ce qu’on pourrait attendre (foo) mais la chaine standard __main__
ce qui explique un idiome qu’on rencontre fréquemment:
# voici un idiome fréquent à la fin d'un source Python
if __name__ == '__main__':
    test_module()voici un exemple
1 2 3 4 5 6 7 8 9 10 11 12 13 14# un module définit souvent des fonctions et classes def fib(n): a, b = 0, 1 while b < n: print(b, end=' ') a, b = b, a + b # le code qui suit sera exécuté seulement si on le lance # depuis la ligne de commande, et pas pendant un import if __name__ == '__main__': fib(40)
dans le dossier samples/
# À la ligne de commande on a
!python samples/fib.py1 1 2 3 5 8 13 21 34 # mais à l'import il ne se passe rien
from samples.fib import fibon peut effectivement utiliser cette fonctionnalité pour faire des tests unitaires, mais ce n’est guère utilisé en production car beaucoup trop limité
de plus, cette pratique entre en conflit avec l’utilisation d’imports relatifs (voir le notebook suivant), aussi elle a tendance à disparaitre au fil du temps
introspection¶
on peut accèder aux attributs d’un module en utilisant
- vars(module)retourne l’espace de nommage de module
- dir(module)liste les attributs
- globals()retourne l’espace de nommage du module courant
remarque locals() retourne l’espace de nommage à l’endroit de l’appel
exemple avec globals()
# cette variable est globale
foo = 10
# celle-ci aussi 
g = globals()
# globals() envoie un dict
type(g)dict# et les globales sont dedans
'foo' in g and 'g' in gTrueg['foo']10# si on n'est pas dans une fonction ou une classe,
# locals() et globals() retournent la même chose
locals() == globals()Trueexemple avec locals()
# par contre dans une fonction c'est différent
def f():
    tutu = 12
    print(f"tutu dans globals ? : {'tutu' in globals()}")
    print(f"tutu dans locals ? : {'tutu' in locals()}")
    print(f"foo dans globals ? : {'foo' in globals()}")
    print(f"foo dans locals ? : {'foo' in locals()}")
f()tutu dans globals ? : False
tutu dans locals ? : True
foo dans globals ? : True
foo dans locals ? : False
recharger un module¶
on a vu plus haut comment configurer IPython pour pouvoir travailler efficacement depuis ipython ou un notebook
en général c’est suffisant, mais si nécessaire on peut aussi utiliser le module standard importlib pour forcer le rechargement d’un module
voici un exemple complet
1 2 3 4eggs = 1 def spam(s): print(s, 'spam')
# je l'importe une première fois
import toplevel
# on y trouve donc
toplevel.eggs1# je peux changer l'intérieur du module
# à nouveau ce n'est pas conseillé, mais c'est légal
toplevel.eggs = 2
toplevel.eggs2# à ce stade si j'importe à nouveau, il ne se passe rien
# car le module est dans le cache
import toplevel
# notamment on a toujours la valeur modifiée de l'attribut
toplevel.eggs2# mais je peux forcer le réimport comme ceci
import importlib
importlib.reload(toplevel)
# et là maintenant je repars d'un module propre
# comme le prouve son attribut
toplevel.eggs1pour les hackers curieux, le cache des modules est rangé dans sys.modules, sous la forme d’un dictionnaire { nom → objet module } de tous les modules chargés
ce qui vous permet d’entrevoir une méthode plus brutale pour forcer le re-chargement des modules...
avec l’instruction import on ne peut importer qu’un nom littéral
mais comment faire si le nom du module est lui-même dans une variable ?
pour cela voyez la fonction importlib.import_module
note: exec est typiquement déconseillé pour ce genre d’usages
import importlib
nom_module = "math"
math2 = importlib.import_module(nom_module)
math2.e2.718281828459045attributs privés¶
il existe une convention de nommage:
tous les noms globaux qui commencent par un underscore (_)
sont privés au module
cela signifie qu’ils ne font pas partie de l’API, et qu’il n’est pas sage de les modifier
ça n’est qu’une convention, mais c’est généralement suffisant