module – Sam & Max http://sametmax.com Du code, du cul Wed, 30 Oct 2019 15:34:04 +0000 en-US hourly 1 https://wordpress.org/?v=4.9.7 32490438 Rendez votre package exécutable avec __main__.py http://sametmax.com/rendez-votre-module-executable-avec-__main__-py/ http://sametmax.com/rendez-votre-module-executable-avec-__main__-py/#comments Tue, 13 Jan 2015 06:47:42 +0000 http://sametmax.com/?p=15704 __init__.py, mais __main__.py est moins connu.]]> Tout le monde connait le fichier __init__.py, mais __main__.py est moins connu.

Il permet de lancer du code si on tente d’exécuter un package (c’est à dire un dossier qui contient un fichier __init__.py):

$ tree monpackage/
monpackakge/
├── __init__.py
└── __main__.py

0 directories, 2 files
$ cat monpackage/__main__.py
print('Hello :)')
$ python monpackage/ # ceci est un dossier 
Hello :)

Le __main__.py est aussi exécuté quand on fait python -m monpackage.

Notez que son exécution suppose l’import préalable du package, et que donc __init__.py sera toujours exécuté avant __main__.py. En revanche, faire juste import monpackage ne déclenche pas l’exécution de __main__.py.

Si vous zippez votre package et appelez la commande python, c’est aussi ce fichier qui sera exécuté. Pratique donc, pour faire un exécutable portable à peu de frais, tout en gardant la lib importable.

]]>
http://sametmax.com/rendez-votre-module-executable-avec-__main__-py/feed/ 5 15704
Les imports en Python http://sametmax.com/les-imports-en-python/ http://sametmax.com/les-imports-en-python/#comments Thu, 16 May 2013 09:13:27 +0000 http://sametmax.com/?p=6127 sys.path.append partout juste au cas où et c'était encore pire. Vous avez donc décidé de vous remettre à PHP, au moins le include utilise les chemins de fichiers, et ça, c'est facile.]]> Je suis fan de carmina burrana depuis l’age de 12 ans, alors pourquoi pas O Fortuna comme musique d’ambiance :

Les imports, c’était fastoche. Vous étiez dans votre petit programme, et pour importer un module de la lib standard, vous faisiez:

import module

Par exemple :

import os

Et pour importer une classe ou une fonction de cette lib, vous faisiez :

from module import fonction
from module import Classe

Par exemple :

from hashlib import md5
from xml.etree import Element

Parfois, c’était un peu plus compliqué, mais ça allait encore. Des fois il fallait importer un sous-module :

from package.sous_package import module

Par exemple :

from xml.sax import saxutils

Mais ça allait encore.

Et puis un jour vous avez du écrire votre propre module. Vous n’aviez pas vraiment réfléchi à la question. C’était juste une petite lib pour regrouper des fonctions. Ou juste une app Django. Un truc tout simple. Mais les imports ont soudainement cessé de devenir clairs. Ça ne marchait pas. Rien ne marchait. Vous aviez des sys.path.append partout juste au cas où et c’était encore pire.

Vous avez donc décidé de vous remettre à PHP, au moins le include utilise les chemins de fichiers, et ça, c’est facile.

Sous le capot

Quand vous utilisez import, sous le capot Python utilise le fonction __import__. Malgré ses __ dans le nom, c’est une fonction ordinaire, et vous pouvez d’ailleurs l’utiliser vous-même :

>>> os = __import__('os')
>>> os.path.join('s', 'ton', 'mon', 'g')
u's/ton/mon/g'

En fait, importer un module, c’est créer un objet module qui est assigné à une variable tout à fait normale :

>>> type(os)

>>> os = "on peut ecraser un module"
>>> os.path
Traceback (most recent call last):
  File "", line 1, in 
    os.path
AttributeError: 'unicode' object has no attribute 'path'

>>> import sys
>>> type(sys)

>>> sys = "je t'ecrase la tronche"
>>> type(sys)

Le mécanisme de module Python n’est donc pas un truc à part, c’est un objet comme le reste, qui contient des attributs. Les attributs sont les variables et les fonctions du module.

Pour charger un module, la fonction __import__ passe par les étapes suivantes :

  1. Chercher si le module os existe.
  2. Chercher si le module a déjà été importé. Si oui, s’arrêter ici et renvoyer le module existant.
  3. Si non, chercher si il a été déjà compilé en .pyc.
  4. Si ce n’est pas le cas, compiler le fichier .py en .pyc.
  5. Charger le bytecode du fichier pyc.
  6. Créer un objet module vide.
  7. Éxecuter le bytecode dans le contexte de l’objet module et remplir ce dernier avec le résultat.
  8. Ajouter l’objet module dans sys.modules, un dictionnaire qui contient tous les modules déjà chargés.
  9. Retourner le module pour pouvoir l’assigner à une variable, par défaut la variable porte son nom.

La fonction __import__ est donc très complexe, et d’ailleurs si vous voulez l’utiliser pour des trucs plus compliqués qu’un simple import de module, vous allez galérer car sa signature est vraiment zarb.

Mais pour vous, seule l’étape 1 est importante à comprendre. C’est l’étape à laquelle tout se joue.

Comment Python définit quel module importer ?

C’est la partie vraiment difficile, en effet si un import ne marche pas, c’est très souvent parce que Python ne trouve pas le module que vous voulez. Et la raison pour laquelle il ne le trouve pas, c’est que vous ne comprenez pas comment il cherche.

Python utilise ce qu’on appelle le PYTHON PATH pour chercher les modules importables. C’est une variable système qui contient une liste de dossiers. Par exemple, sur ma machine, elle contient ceci :

['',
 '/usr/bin',
 '/usr/local/lib/python2.7/dist-packages/grin-1.2.1-py2.7.egg',
 '/usr/lib/python2.7',
 '/usr/lib/python2.7/plat-linux2',
 '/usr/lib/python2.7/lib-tk',
 '/usr/lib/python2.7/lib-old',
 '/usr/lib/python2.7/lib-dynload',
 '/home/sam/.local/lib/python2.7/site-packages',
 '/usr/local/lib/python2.7/dist-packages',
 '/usr/local/lib/python2.7/dist-packages/setuptools-0.6c11-py2.7.egg-info',
 '/usr/lib/python2.7/dist-packages',
 '/usr/lib/python2.7/dist-packages/PIL',
 '/usr/lib/python2.7/dist-packages/gst-0.10',
 '/usr/lib/python2.7/dist-packages/gtk-2.0',
 '/usr/lib/pymodules/python2.7',
 '/usr/lib/python2.7/dist-packages/ubuntu-sso-client',
 '/usr/lib/python2.7/dist-packages/ubuntuone-client',
 '/usr/lib/python2.7/dist-packages/ubuntuone-control-panel',
 '/usr/lib/python2.7/dist-packages/ubuntuone-couch',
 '/usr/lib/python2.7/dist-packages/ubuntuone-installer',
 '/usr/lib/python2.7/dist-packages/ubuntuone-storage-protocol',
 '/usr/lib/python2.7/dist-packages/wx-2.6-gtk2-unicode',
 '/usr/lib/python2.7/dist-packages/IPython/extensions']

Donc, quand vous faites import os, Python va faire une boucle for là dessus et chercher dans chaque dossier si un package (un dossier avec un fichier __init__.py) ou un module (un fichier avec l’extension .py) nommé os existe.

Dès qu’il en trouve un, il s’arrête de chercher et l’importe. Si il n’en trouve pas, il va lever une ImportError.

Ce qui signifie que si votre module n’est PAS dans le PYTHON PATH, vous ne pouvez PAS l’importer. C’est impossible.

La grande majorité des problèmes d’import vient du fait que le module que vous essayez d’importer n’est pas dans le PYTHON PATH.

Maintenant, la grande question, c’est :

Qu’est-ce qui est dans le PYTHON PATH ?

Par défault, les dossiers sites-packages et dist-packages dans le dossier d’installation Python sont dans le PYTHON PATH. Quelques autres sont ajoutés selon les systèmes, mais vous pouvez toujours compter sur sites-packages et dist-packages pour être dans le PYTHON PATH. Quand vous installez une lib, par exemple avec pip, c’est là dedans que la lib va s’installer, pour être sûre de pouvoir être importée.

Quand vous êtes dans un virtualenv, les dossiers sites-packages et dist-packages de l’environnement virtuel sont ajoutés au PYTHON PATH.

Mais tout ça ne change pas grand chose pour vous. En effet, vous n’allez pas mettre VOTRE code dans les dossiers sites-packages et dist-packages.

C’est pour cela que Python possède un mécanisme supplémentaire : le dossier qui contient le module sur lequel vous lancez la commande python est automatiquement ajouté au PYTHON PATH.

Le PYTHON PATH, en pratique

Supposons que je sois dans le dossier /home/sam/Bureau et que j’aie dedans ce package. Voici à quoi ressemble mon arbo (téléchargez l’arbo vierge pour vos tests):

/home/sam/Bureau # <-- je suis ici
.
`-- test_imports
    |-- __init__.py
    |-- package_tout_en_haut
    |   |-- __init__.py
    |   |-- autre_sous_package
    |   |   |-- __init__.py
    |   |   `-- autre_module_en_bas.py
    |   |-- sous_module.py
    |   `-- sous_package
    |       |-- __init__.py
    |       |-- autre_module_en_bas.py
    |       |-- autre_sous_package
    |       |   |-- __init__.py
    |       |   `-- autre_module_en_bas.py
    |       `-- module_tout_en_bas.py
    `-- top_module.py

Si je lance un shell Python depuis ce dossier ou un script Python contenu dans ce dossier, je peux faire import test_imports, car /home/sam/Bureau est automatiquement ajouté au PYTHON PATH.

Je peux donc faire :

>>> import test_imports
>>> from test_imports import package_tout_en_haut
>>> from test_imports import top_module
test_imports.top_module
>>> from test_imports.package_tout_en_haut import sous_module
test_imports.package_tout_en_haut.sous_module

Mais si je me mets ici dans ./package_tout_en_haut/sous_package :

/home/sam/Bureau
.
`-- test_imports
    |-- __init__.py
    |-- package_tout_en_haut
    |   |-- __init__.py
    |   |-- autre_sous_package
    |   |   |-- __init__.py
    |   |   `-- autre_module_en_bas.py
    |   |-- sous_module.py
    |   `-- sous_package     # <-- je suis ici
    |       |-- __init__.py
    |       |-- autre_module_en_bas.py
    |       |-- autre_sous_package
    |       |   |-- __init__.py
    |       |   `-- autre_module_en_bas.py
    |       |-- module_tout_en_bas.py
    `-- top_module.py

Je ne peux PAS importer test_imports, ni dans un shell, ni depuis un module de ce dossier :

>>> import test_imports
Traceback (most recent call last):
  File "", line 1, in 
ImportError: No module named test_imports

En effet, comme je lance la commande Python depuis

./package_tout_en_haut/sous_package

alors

./package_tout_en_haut/sous_package

EST ajouté au PYTHON PATH, mais

/home/sam/Bureau/

n’est PAS ajouté au PYTHON PATH.

Je ne peux donc PAS faire

from test_imports import top_module 

depuis un fichier comme

.test_imports/package_tout_en_haut/sous_package/autre_module_en_bas.py

et exécuter directement

python autre_module_en_bas.py

ni même

python ./test_imports/package_tout_en_haut/sous_package/autre_module_en_bas.py

Je peux faire

from test_imports import top_module

depuis

autre_module_en_bas.py

uniquement si je lance un script Python tout en haut de mon arbo qui importe

autre_module_en_bas.py.

Mais alors, comment on fait ?

Il faut s’assurer que le dossier qui contient test_imports, notre module racine, soit TOUJOURS dans le PYTHON PATH.

Il y a plusieurs possibilités pour cela.

La première, c’est que notre lib va être utilisée une fois installée avec pip. Dans ce cas, on s’en branle, test_imports sera dans sites-packages automatiquement, et on pourra faire from test_imports import top_module de partout joyeusement.

Mais souvent, ce n’est pas le cas, votre code n’est pas fait pour être installé.

La seconde technique consiste à s’assurer que l’on appelle TOUJOURS la commande Python depuis le dossier qui est tout au dessus. C’est ce que fait django avec sa commande ./manage.py par exemple.

Vous avez votre projet :

./manage.py
projet

Et tout passe par python manage.py, qui est au dessus de projet, donc le dossier est bien ajouté au PYTHON PATH, et tout va bien.

Dans votre cas ça veut dire vous assurer qu’on lance toujours votre programme depuis un script d’entrée qui est tout en haut de votre arborescence.

Ca veut dire que vous devez avoir un point d’entrée UNIQUE pour votre package.

Mais parfois ça ne convient pas. Dans le cas des tests unitaires par exemple, il vous faut un point d’entrée spécialement pour les tests.

Pour ce genre de scénario, il faut donc avoir le dossier qui les contient à côté de votre package. Ainsi, si j’avais des tests unitaires, je devrais faire un dossier tests à côté du dossier test_imports. Par exemple, transformer mon arbo en un truc comme ça :

src
   |_ test_imports
   |_ tests

Afin que je lance les tests en faisant python tests depuis src. Et dans mes fichiers de tests, je pourrai faire des from test_imports import truc.

La manière dont vous organisez votre projet est donc très importante en Python, et si vous avez des problèmes d’import, la première chose à faire est de changer sa structure. Il n’y a pas de magie.

La dernière possibilité, quand tout a échoué, c’est de rajouter à la main le dossier dans le PYTHON PATH. sys.path est une simple liste, on peut donc faire un append() dessus.

Par exemple, si je veux absolument (mais je ne devrais pas :-)) pouvoir faire :

python .test_imports/package_tout_en_haut/sous_package/autre_module_en_bas.py et importer test_imports dans autre_module_en_bas.py, je peux faire un truc du genre :

import os

dossier = os.path.dirname(os.path.abspath(__file__))

while not dossier.endswith('test_imports'):
    dossier = os.path.dirname(dossier)

dossier = os.path.dirname(dossier)

if dossier not in sys.path:
    sys.path.append(dossier)

Ce code va remonter dans l’arbo jusqu’à tomber sur le chemin du dossier test_imports et ajouter son dossier parent au PYTHON PATH.

Ce n’est pas le truc le plus propre du monde, mais ça peut dépanner.

Imports absolus et relatifs

Si vous êtes dans ./package_tout_en_haut/sous_package :

/home/sam/Bureau
.
`-- test_imports
    |-- __init__.py
    |-- package_tout_en_haut
    |   |-- __init__.py
    |   |-- autre_sous_package
    |   |   |-- __init__.py
    |   |   `-- autre_module_en_bas.py
    |   |-- sous_module.py
    |   `-- sous_package     # <-- je suis ici
    |       |-- __init__.py
    |       |-- autre_module_en_bas.py
    |       |-- autre_sous_package
    |       |   |-- __init__.py
    |       |   `-- autre_module_en_bas.py
    |       |-- module_tout_en_bas.py
    |       `-- test_imports  # <-- autre package nommé test_imports
    |           `-- sous_module.py
    `-- top_module.py

Vous voyez que vous avez deux packages nommés test_imports.

Si vous écrivez import test_imports dans autre_module_en_bas.py, que va-t-il se passer ?

C'est le module tout en bas qui va être importé.

Ce n'est pas forcément ce que vous voulez. Python 3 corrige cela en permettant des imports relatifs, et Python 2.7 peut en bénéficier en important tout en haut du module :

from __future__ import absolute_import

En faisant cela, vous obtenez le comportement de Python 3 dans Python 2.7, et vous pourrez alors choisir entre faire :

import test_imports # importe le module tout en haut
from . import test_imports # import le module dans le même dossier
from .test_imports import sous_module
from test_imports import top_module

Je vous recommande de toujours utiliser from __future__ import absolute_import. Ca ne coûte rien, et c'est plus cohérent. Par contre, vous ne pourrez pas tester from __future__ import absolute_import dans le shell, donc cet exemple ne marche pas dans ipython, mais il fonctionne parfaitement dans vos modules.

On peut aussi faire des imports relatifs du package contenant avec :

from .. import truc
from ..package import machin

N'oubliez pas que ceci ne marche que :

  • Si from __future__ import absolute_import est activé.
  • Le package tout en haut (celui qui contient tous les autres) est dans le PYTHON PATH

Sinon, ça ne sert A RIEN. Ce n'est pas comme un ../ dans un bash. Ça ne remonte pas d'un dossier. C'est juste une notation pour dire j'utilise celui la plutôt que l'autre, quand il y a ambiguité.

Pièges des imports

Package sans init

Si vous avez :

.
`-- test_imports
    |-- __init__.py
    |-- package_sans_init
    |   `-- nada.py

nada.py n'est pas importable, car package_sans_init ne contient pas de fichier __init__.py, même si test_imports est dans le PYTHON PATH. Ce comportement est corrigé en Python 3, et tout sous-dossier d'un package importable est automatiquement importable, qu'il contienne un __init__.py ou non.

Imports circulaires

J'en ai déjà parlé ici.

Vous avez :

.
`-- test_imports
    |-- __init__.py
    |-- package_tout_en_haut
    |   |-- __init__.py
    |   `-- sous_package
    |       |-- __init__.py
    |       |-- autre_module_en_bas.py
    |       `-- module_tout_en_bas.py

Et vous importez autre_module_en_bas dans module_tout_en_bas et inversement. Non seulement ça ne marchera pas, mais en plus l'erreur est déroutante :

ImportError: No module named module_tout_en_bas

Oui vous avez bien lu, il va vous dire que le module n'existe pas !

Il n'y a pas non plus de solution propre à ce problème : soit vous fusionnez vos deux fichiers, soit vous faites un 3eme module qui utilise ces deux modules (et ces deux modules n'importent pas ce 3eme module).

Sinon il y a la solution crade : mettre un des imports dans un appel de fonction ou de méthode comme ça:

def truc():
    import module_tout_en_bas
    module_tout_en_bas.bidule()

Parfois, ça dépanne :-) On ne tue pas des chatons non plus, donc si ça ne devient pas une habitude, ça peut passer.

]]>
http://sametmax.com/les-imports-en-python/feed/ 37 6127