Skip to article frontmatterSkip to article content

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:

c’est quoi un module ?

regardons ce qu’il y a dans ce fichier mod.py

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.GLOBALE
100
# ou encore 

mod.spam('good')
dans mod.spam, data=good

rappel: la notion d’attribut

on l’a déjà vu, mais pour rappel

plusieurs formes d’import

la forme import mod est la plus basique; il en existe des variantes:

code

définit la variable

commentaire

import mod

mod

de type module, avec des attributs

import mod as mymod

mymod

à part ça, même effet que import mod

from mod import spam

spam

on n’a pas accès au module, seulement un attribut
qui est directement accessible via la variable spam

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 Foo

on retrouve l’équivalent dans l’espace des modules

pack1
pack1.pack2
pack1.pack2.mod
pack1.pack2.mod.Foo

importer 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

mod.py
1
2
3
4
FOO = "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.mod
pack1 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

que fait une importation ?

principalement deux choses:

  1. vérifier si le module est déjà chargé, et sinon:

  1. 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:

l’algorithme

du coup on a choisi de chercher les fichiers dans l’ordre suivant

  1. d’abord on cherche dans le dossier où se trouve le point d’entrée du programme

  2. ensuite, si elle est définie, dans le ou les dossiers configurés dans la variable d’environnement PYTHONPATH

  3. 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

les fausses bonnes idées

du monolithe au package

pour vos premiers pas, vous allez en général compliquer progressivement:

toutefois, cette approche atteint rapidement sa limite, car

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 :)

notions avancées

références partagées

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 = 10

exécuter un module comme un script

un module peut avoir deux rôles

chaque module a un attribut __name__ qui est défini par l’import

toplevel.py
1
2
3
4
eggs = 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

fib.py
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.py
1 1 2 3 5 8 13 21 34 
# mais à l'import il ne se passe rien
from samples.fib import fib

introspection

on peut accèder aux attributs d’un module en utilisant

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 g
True
g['foo']
10
# si on n'est pas dans une fonction ou une classe,
# locals() et globals() retournent la même chose
locals() == globals()
True

exemple 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

toplevel.py
1
2
3
4
eggs = 1

def spam(s):
    print(s, 'spam')
# je l'importe une première fois
import toplevel

# on y trouve donc
toplevel.eggs
1
# je peux changer l'intérieur du module
# à nouveau ce n'est pas conseillé, mais c'est légal

toplevel.eggs = 2
toplevel.eggs
2
# à 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.eggs
2
# 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.eggs
1
import importlib
nom_module = "math"
math2 = importlib.import_module(nom_module)
math2.e
2.718281828459045

attributs 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