Formation gratuite Python
- Construire un module d’extension C Python – Real Python
- Optimisez votre code avec des valeurs de vérité – Real Python
- Créez un jeu de plateforme en Python avec Arcade – Real Python
- Mise à jour d'un plug-in de publication dans l'éditeur de blocs • WPShout
- Simplifier le bouclage avec des compteurs – Real Python
En Python, vous utilisez le importer
mot-clé pour faire du code en un module disponible dans un autre. Les importations en Python sont importantes pour structurer votre code effectivement. Utiliser correctement les importations vous rendra plus productif, vous permettant de réutiliser le code tout en gardant vos projets maintenables.
Ce didacticiel fournira un aperçu complet de Python importer
déclaration et comment cela fonctionne. Le système d'importation est puissant et vous apprendrez à exploiter cette puissance. Bien que vous couvrirez de nombreux concepts derrière le système d'importation de Python, ce didacticiel est principalement basé sur des exemples. Vous apprendrez à travers plusieurs exemples de code.
Tout au long du didacticiel, vous verrez des exemples sur la façon de jouer avec la machinerie d'importation Python afin de travailler plus efficacement. Alors que tout le code est affiché dans le tutoriel, vous pouvez également le télécharger en cliquant sur la case ci-dessous:
Python basique importer
Le code Python est organisé à la fois en modules et en packages. Cette section explique en quoi ils diffèrent et comment vous pouvez travailler avec eux.
Plus loin dans le didacticiel, vous verrez des utilisations avancées et moins connues du système d'importation de Python. Cependant, commençons par les bases: importer des modules et des packages.
Modules
Le glossaire Python.org définit module comme suit:
Un objet qui sert d'unité organisationnelle de code Python. Les modules ont un espace de noms contenant des objets Python arbitraires. Les modules sont chargés dans Python par le processus d'importation. (La source)
En pratique, un module correspond généralement à un .py
fichier contenant du code Python.
La véritable puissance des modules est qu'ils peuvent être importés et réutilisés dans un autre code. Prenons l'exemple suivant:
>>> importer math
>>> math.pi
3.141592653589793
En première ligne, importer des mathématiques
, vous importez le code dans le math
module et le rendre disponible à utiliser. Sur la deuxième ligne, vous accédez au pi
variable dans le math
module. math
fait partie de la bibliothèque standard de Python, ce qui signifie qu'il est toujours disponible pour l'importation lorsque vous exécutez Python.
Notez que vous écrivez math.pi
et pas simplement pi
. En plus d'être un module, math
agit comme espace de noms qui garde tous les attributs du module ensemble. Les espaces de noms sont utiles pour garder votre code lisible et organisé. Pour reprendre les mots de Tim Peters:
Les espaces de noms sont une excellente idée de klaxon – faisons-en plus! (La source)
Vous pouvez répertorier le contenu d'un espace de noms avec dir ()
:
>>> importer math
>>> dir()
['__annotations__', '__builtins__', ..., 'math']
>>> dir(math)
['__doc__', ..., 'nan', 'pi', 'pow', ...]
En utilisant dir ()
sans aucun argument montre ce qui se trouve dans l'espace de noms global. Pour voir le contenu du math
espace de noms, vous utilisez dir (math)
.
Vous avez déjà vu l'utilisation la plus simple de importer
. Cependant, il existe d'autres façons de l'utiliser qui vous permettent d'importer des parties spécifiques d'un module et de renommer le module lorsque vous l'importez.
Le code suivant importe uniquement le pi
variable de la math
module:
>>> de math importer pi
>>> pi
3.141592653589793
>>> math.pi
NameError: le nom 'math' n'est pas défini
Notez que ce lieu pi
dans l'espace de noms global et non dans un math
espace de noms.
Vous pouvez également renommer les modules et les attributs lors de leur importation:
>>> importer math comme m
>>> m.pi
3.141592653589793
>>> de math importer pi comme PI
>>> PI
3.141592653589793
Pour plus de détails sur la syntaxe d'importation des modules, consultez Modules et packages Python – Une introduction.
Paquets
Vous pouvez utiliser un package pour organiser davantage vos modules. Le glossaire Python.org définit paquet comme suit:
Un module Python qui peut contenir des sous-modules ou récursivement, des sous-packages. Techniquement, un package est un module Python avec un
__chemin__
attribut. (La source)
Notez qu'un package est toujours un module. En tant qu'utilisateur, vous n'avez généralement pas à vous soucier de savoir si vous importez un module ou un package.
En pratique, un package correspond généralement à un répertoire de fichiers contenant des fichiers Python et d'autres répertoires. Pour créer vous-même un package Python, vous créez un répertoire et un fichier nommé __init__.py
à l'intérieur. le __init__.py
Le fichier contient le contenu du package lorsqu'il est traité comme un module. Il peut être laissé vide.
Remarque: Répertoires sans __init__.py
Les fichiers sont toujours traités comme des packages par Python. Cependant, ce ne seront pas des packages réguliers, mais quelque chose appelé packages d'espace de noms. Vous en apprendrez plus à leur sujet plus tard.
En général, les sous-modules et sous-packages ne sont pas importés lorsque vous importez un package. Cependant, vous pouvez utiliser __init__.py
pour inclure tout ou partie des sous-modules et sous-packages si vous le souhaitez. Pour afficher quelques exemples de ce comportement, vous allez créer un package pour dire Bonjour le monde
dans quelques langues différentes. Le package sera composé des répertoires et fichiers suivants:
monde/
│
├── afrique /
│ ├── __init__.py
│ └── zimbabwe.py
│
├── europe /
│ ├── __init__.py
│ ├── greece.py
│ ├── norway.py
│ └── spain.py
│
└── __init__.py
Chaque fichier de pays imprime un message d'accueil, tandis que __init__.py
les fichiers importent sélectivement certains des sous-packages et sous-modules. Le contenu exact des fichiers est le suivant:
# world / africa / __ init__.py (fichier vide)
# monde / afrique / zimbabwe.py
impression("Shona: Mhoroyi vhanu vese")
impression("Ndebele: Sabona mhlaba")
# world / europe / __ init__.py
de . importer Grèce
de . importer Norvège
# monde / europe / greece.py
impression("Grec: Γειά σας Κόσμε")
# monde / europe / norway.py
impression("Norvégien: Hei verden")
# monde / europe / espagne.py
impression("Castellano: Hola mundo")
# world / __ init__.py
de . importer Afrique
Notez que monde / __ init__.py
importations uniquement Afrique
et pas L'Europe
. De même, monde / afrique / __ init__.py
n’importe rien, alors que monde / europe / __ init__.py
importations Grèce
et Norvège
mais non Espagne
. Chaque module de pays imprimera un message d'accueil lors de son importation.
Jouons avec le monde
à l'invite interactive pour mieux comprendre le comportement des sous-packages et des sous-modules:
>>> importer monde
>>> monde
>>> # Le sous-paquetage Afrique a été automatiquement importé
>>> monde.Afrique
>>> # Le sous-paquet europe n'a pas été importé
>>> monde.L'Europe
AttributeError: le module 'world' n'a pas d'attribut 'europe'
Quand L'Europe
est importé, le europe.greece
et europe.norway
les modules sont également importés. Vous pouvez le voir parce que les modules de pays impriment un message d'accueil lorsqu'ils sont importés:
>>> # Importer l'Europe explicitement
>>> de monde importer L'Europe
Grec: Γειά σας Κόσμε
Norvégien: Hei verden
>>> # Le sous-module Grèce a été importé automatiquement
>>> L'Europe .Grèce
>>> # Parce que le monde est importé, l'europe se trouve également dans l'espace de noms mondial
>>> monde.L'Europe .Norvège
>>> # Le sous-module Espagne n'a pas été importé
>>> L'Europe .Espagne
AttributeError: le module 'world.europe' n'a pas d'attribut 'espagne'
>>> # Importer l'espagne explicitement dans l'espace de noms du monde
>>> importer world.europe.spain
Castellano: Hola mundo
>>> # Notez que l'espagne est également disponible directement dans l'espace de noms europe
>>> L'Europe .Espagne
>>> # L'importation de la Norvège ne fait pas à nouveau l'importation (pas de sortie), mais ajoute
>>> # norvège à l'espace de noms global
>>> de world.europe importer Norvège
>>> Norvège
le monde / afrique / __ init__.py
le fichier est vide. Cela signifie que l'importation du world.africa
le package crée l'espace de noms mais n'a aucun autre effet:
>>> # Même si l'afrique a été importée, le zimbabwe n'a pas
>>> monde.Afrique.Zimbabwe
AttributeError: le module 'world.africa' n'a pas d'attribut 'zimbabwe'
>>> # Importer le Zimbabwe explicitement dans l'espace de noms global
>>> de world.africa importer Zimbabwe
Shona: Mhoroyi vhanu vese
Ndebele: Sabona mhlaba
>>> # Le sous-module zimbabwe est maintenant disponible
>>> Zimbabwe
>>> # Notez que le Zimbabwe peut également être atteint via le sous-paquet Afrique
>>> monde.Afrique.Zimbabwe
N'oubliez pas que l'importation d'un module charge le contenu et crée un espace de noms contenant le contenu. Les derniers exemples montrent qu’il est possible que le même module fasse partie d’espaces de noms différents.
Détail technique: L'espace de noms du module est implémenté en tant que dictionnaire Python et est disponible sur le site .__ dict__
attribut:
>>> importer math
>>> math.__dict__[[[["pi"]
3.141592653589793
Vous avez rarement besoin d'interagir avec .__ dict__
directement.
De même, l'espace de noms global de Python est également un dictionnaire. Vous pouvez y accéder via globaux ()
.
Il est assez courant d'importer des sous-packages et des sous-modules dans un __init__.py
pour les rendre plus facilement accessibles à vos utilisateurs. Vous pouvez voir un exemple de cela dans le populaire demandes
paquet.
Importations absolues et relatives
Rappelez le code source de monde / __ init__.py
dans l'exemple précédent:
Vous avez déjà vu de ... import
des déclarations telles que de math import pi
, mais qu'est-ce que le point (.
) dans de . importer l'afrique
signifier?
Le point fait référence au package actuel, et la déclaration est un exemple de importation relative. Vous pouvez le lire comme «À partir du package actuel, importez le sous-package Afrique
. "
Il y a un équivalent importation absolue dans laquelle vous nommez explicitement le package actuel:
En fait, toutes les importations monde
aurait pu être fait explicitement avec des importations absolues similaires.
Les importations relatives doivent être sous la forme de ... import
et l'emplacement à partir duquel vous importez doit commencer par un point.
Le guide de style PEP 8 recommande d'utiliser les importations absolues en général. Cependant, les importations relatives sont une alternative pour organiser les hiérarchies de packages. Pour plus d'informations, consultez Importations absolues et importations relatives en Python.
Chemin d'importation de Python
Comment Python trouve-t-il les modules et les packages qu'il importe? Vous verrez plus de détails sur la mécanique du système d'importation Python plus tard. Pour l'instant, sachez simplement que Python recherche des modules et des packages dans son chemin d'importation. Il s'agit d'une liste d'emplacements recherchés pour les modules à importer.
Remarque: Lorsque vous tapez importer quelque chose
, Python recherchera quelque chose
quelques endroits différents avant de rechercher le chemin d'importation.
En particulier, il cherchera dans un cache de module pour voir si quelque chose
a déjà été importé et il effectuera une recherche parmi les modules intégrés.
Vous en apprendrez plus sur le mécanisme complet d'importation Python dans une section ultérieure.
Vous pouvez inspecter le chemin d'importation de Python en imprimant sys.path
. D'une manière générale, cette liste contiendra trois types d'emplacements différents:
- Le répertoire du script actuel (ou le répertoire actuel s'il n'y a pas de script, comme lorsque Python s'exécute de manière interactive)
- Le contenu du
PYTHONPATH
variable d'environnement - Autres répertoires, dépendants de l'installation
En règle générale, Python démarre au début de la liste des emplacements et recherche un module donné dans chaque emplacement jusqu'à la première correspondance. Étant donné que le répertoire de script ou le répertoire actuel est toujours le premier dans cette liste, vous pouvez vous assurer que vos scripts trouvent vos modules et packages auto-créés en organisant vos répertoires et en faisant attention à partir de quel répertoire vous exécutez Python.
Cependant, vous devez également faire attention à ne pas créer de modules ombreou masquer d'autres modules importants. Par exemple, disons que vous définissez les éléments suivants math
module:
# math.py
def double(nombre):
revenir 2 * nombre
L'utilisation de ce module fonctionne comme prévu:
>>> importer math
>>> math.double(3.14)
6.28
Mais ce module fait également de l'ombre math
module inclus dans la bibliothèque standard. Malheureusement, cela signifie que notre exemple précédent de recherche de la valeur de π ne fonctionne plus:
>>> importer math
>>> math.pi
Traceback (dernier appel le plus récent):
Fichier "" , ligne 1, dans
AttributeError: le module 'math' n'a pas d'attribut 'pi'
>>> math
Le problème est que Python recherche maintenant votre nouveau math
module pour pi
au lieu de chercher le math
dans la bibliothèque standard.
Pour éviter ce genre de problèmes, vous devez faire attention aux noms de vos modules et packages. En particulier, vos noms de module et de package de niveau supérieur doivent être uniques. Si math
est défini comme un sous-module dans un package, il n'obscurcira pas le module intégré.
Exemple: structurez vos importations
Bien qu'il soit possible d'organiser vos importations en utilisant le répertoire actuel ainsi qu'en manipulant PYTHONPATH
et même sys.path
, le processus est souvent indiscipliné et sujet à des erreurs. Pour voir un exemple typique, considérez l'application suivante:
structure/
│
├── files.py
└── structure.py
L'application recréera une structure de fichiers donnée en créant des répertoires et des fichiers vides. le structure.py
le fichier contient le script principal, et files.py
est un module de bibliothèque avec quelques fonctions pour traiter les fichiers. Voici un exemple de sortie de l'application, dans ce cas en l'exécutant dans le structure
annuaire:
$ python structure.py.
Créer un fichier: /home/gahjelle/structure/001/structure.py
Créer un fichier: /home/gahjelle/structure/001/files.py
Créer un fichier: /home/gahjelle/structure/001/__pycache__/files.cpython-38.pyc
Les deux fichiers de code source ainsi que les fichiers créés automatiquement .pyc
les fichiers sont recréés dans un nouveau répertoire nommé 001
.
Jetez maintenant un œil au code source. La fonctionnalité principale de l'application est définie dans structure.py
:
1 # structure / structure.py
2
3 # Importations de bibliothèques standard
4 importer pathlib
5 importer sys
6
7 # Importations locales
8 importer des dossiers
9
dix def principale():
11 # Lire le chemin depuis la ligne de commande
12 essayer:
13 racine = pathlib.Chemin(sys.argv[[[[1]).résoudre()
14 sauf IndexError:
15 impression("Besoin d'un argument: la racine de l'arborescence de fichiers d'origine")
16 élever SystemExit()
17
18 # Recréez la structure du fichier
19 new_root = des dossiers.chemin_unique(pathlib.Chemin.cwd(), ": 03d")
20 pour chemin dans racine.rglob("*"):
21 si chemin.is_file() et new_root ne pas dans chemin.Parents:
22 rel_path = chemin.relatif à(racine)
23 des dossiers.add_empty_file(new_root / rel_path)
24
25 si __Nom__ == "__principale__":
26 principale()
Dans lignes 12 à 16, vous lisez un chemin racine à partir de la ligne de commande. Dans l'exemple ci-dessus, vous utilisez un point, ce qui signifie le répertoire courant. Ce chemin sera utilisé comme racine
de la hiérarchie de fichiers que vous allez recréer.
Le travail réel se déroule dans lignes 19 à 23. Tout d'abord, vous créez un chemin unique, new_root
, ce sera la racine de votre nouvelle hiérarchie de fichiers. Ensuite, vous parcourez tous les chemins en dessous de l'original racine
et recréez-les en tant que fichiers vides dans la nouvelle hiérarchie de fichiers.
Pour manipuler des chemins comme celui-ci, pathlib
dans la bibliothèque standard est très utile. Pour plus de détails sur son utilisation, consultez Python 3 pathlib
Module: Apprivoiser le système de fichiers.
Sur ligne 26, tu appelles principale()
. Vous en apprendrez plus sur le si
test sur ligne 25 plus tard. Pour l'instant, vous devez savoir que la variable spéciale __Nom__
a la valeur __principale__
à l'intérieur des scripts, mais il obtient le nom du module à l'intérieur des modules importés. Pour plus d'informations sur __Nom__
, consultez Définir les fonctions principales en Python.
Notez que vous importez des dossiers
sur ligne 8. Ce module de bibliothèque contient deux fonctions utilitaires:
# structure / files.py
def chemin_unique(annuaire, nom_motif):
"" "Rechercher un chemin d'accès qui n'existe pas déjà" ""
compteur = 0
tandis que Vrai:
compteur + = 1
chemin = annuaire / nom_motif.format(compteur)
si ne pas chemin.existe():
revenir chemin
def add_empty_file(chemin):
"" "Créez un fichier vide sur le chemin indiqué" ""
impression(F"Créer un fichier: chemin")
chemin.parent.mkdir(Parents=Vrai, exist_ok=Vrai)
chemin.toucher()
chemin_unique ()
utilise un compteur pour trouver un chemin qui n’existe pas déjà. Dans l'application, vous l'utilisez pour trouver un sous-répertoire unique à utiliser comme new_root
de la hiérarchie des fichiers recréés. Prochain, add_empty_file ()
s'assure que tous les répertoires nécessaires sont créés avant de créer un fichier vide à l'aide .toucher()
.
Jetez un œil à l'importation de des dossiers
encore:
7 # Importations locales
8 importer des dossiers
Ça a l'air tout à fait innocent. Cependant, à mesure que le projet se développe, cette ligne vous causera des maux de tête. Même si vous importez des dossiers
du structure
projet, l'importation est absolu: ça ne commence pas par un point. Cela signifie que des dossiers
doit se trouver dans le chemin d'importation pour que l'importation fonctionne.
Heureusement, le répertoire contenant le script actuel est toujours dans le chemin d'importation de Python, donc cela fonctionne bien pour l'instant. Cependant, si votre projet gagne du terrain, il peut être utilisé de différentes manières.
Par exemple, quelqu'un peut vouloir importer le script dans un bloc-notes Jupyter et l'exécuter à partir de là. Ou ils peuvent vouloir réutiliser le des dossiers
bibliothèque dans un autre projet. Ils peuvent même créer un exécutable avec PyInstaller pour le distribuer plus facilement. Malheureusement, l'un de ces scénarios peut créer des problèmes avec l'importation de des dossiers
.
Pour voir un exemple, vous pouvez suivre le guide PyInstaller et créer un point d'entrée pour votre application. Ajoutez un répertoire supplémentaire en dehors de votre répertoire d'application:
structure/
│
├── structure /
│ ├── files.py
│ └── structure.py
│
└── cli.py
Dans le répertoire externe, créez le script de point d'entrée, cli.py
:
# cli.py
de structure.structure importer principale
si __Nom__ == "__principale__":
principale()
Ce script importera principale()
à partir de votre script d'origine et exécutez-le. Notez que principale()
n'est pas exécuté lorsque structure
est importé en raison de la si
test sur ligne 25 dans structure.py
. Cela signifie que vous devez exécuter principale()
explicitement.
En théorie, cela devrait fonctionner de manière similaire à l'exécution directe de l'application:
$ structure python cli.py
Traceback (dernier appel le plus récent):
Fichier "cli.py", ligne 1, dans
de structure.structure import main
Fichier "/home/gahjelle/structure/structure/structure.py", ligne 8, dans
importer des fichiers
ModuleNotFoundError: aucun module nommé 'files'
Pourquoi cela n'a-t-il pas fonctionné? Du coup, l’importation de des dossiers
déclenche une erreur.
Le problème est qu'en démarrant l'application avec cli.py
, vous avez modifié l'emplacement du script actuel, ce qui modifie à son tour le chemin d'importation. des dossiers
n’est plus sur le chemin d’importation, il ne peut donc pas être importé de façon absolue.
Une solution possible consiste à modifier le chemin d'importation de Python:
7 # Importations locales
8 sys.chemin.insérer(0, str(pathlib.Chemin(__fichier__).parent))
9 importer des dossiers
Cela fonctionne car le chemin d'importation inclut le dossier contenant structure.py
et files.py
. Le problème avec cette approche est que votre chemin d'importation peut devenir très compliqué et difficile à comprendre.
En pratique, vous recréez une fonctionnalité des premières versions de Python appelée importations relatives implicites. Ceux-ci ont été supprimés de la langue par le PEP 328 avec la justification suivante:
Dans Python 2.4 et versions antérieures, si vous lisez un module situé à l'intérieur d'un package, il n'est pas clair si
importer foo
fait référence à un module de niveau supérieur ou à un autre module à l'intérieur du package. Au fur et à mesure que la bibliothèque de Python se développe, de plus en plus de modules internes aux packages existants ombrent soudainement les modules de bibliothèque standard par accident. C’est un problème particulièrement difficile à l’intérieur des packages car il n’existe aucun moyen de spécifier à quel module (La source)
Une autre solution consiste à utiliser une importation relative à la place. Modifier l'importation dans structure.py
comme suit:
7 # Importations locales
8 de . importer des dossiers
Vous pouvez maintenant démarrer votre application via le script de point d'entrée:
$ structure python cli.py
Créer un fichier: /home/gahjelle/structure/001/structure.py
Créer un fichier: /home/gahjelle/structure/001/files.py
Créer un fichier: /home/gahjelle/structure/001/__pycache__/structure.cpython-38.pyc
Créer un fichier: /home/gahjelle/structure/001/__pycache__/files.cpython-38.pyc
Malheureusement, vous ne pouvez plus appeler directement l'application:
$ python structure.py.
Traceback (dernier appel le plus récent):
Fichier "structure.py", ligne 8, dans
de . importer des fichiers
ImportError: impossible d'importer des fichiers de noms depuis '__main__' (structure.py)
Le problème est que les importations relatives sont résolues différemment dans les scripts que les modules importés. Bien sûr, vous pouvez revenir en arrière et restaurer l'importation absolue avant d'exécuter directement le script, ou vous pouvez même faire quelques essayez ... sauf
acrobaties pour importer des fichiers absolument ou relativement selon ce qui fonctionne.
Il existe même un hack officiellement approuvé pour faire fonctionner les importations relatives dans les scripts. Malheureusement, cela vous oblige également à changer sys.path
dans la plupart des cas. Citer Raymond Hettinger:
Il doit y avoir une meilleure façon! (La source)
En effet, une solution meilleure et plus stable consiste à jouer avec le système d'importation et de conditionnement de Python et à installer votre projet en tant que package local en utilisant pépin
.
Créer et installer un package local
Lorsque vous installez un package à partir de PyPI, ce package est disponible pour tous les scripts de votre environnement. Cependant, vous pouvez également installer des packages à partir de votre ordinateur local, et ils seront également disponibles de la même manière.
La création d'un package local n'implique pas beaucoup de frais généraux. Tout d'abord, créez un minimum setup.cfg
et setup.py
fichiers à l'extérieur structure
annuaire:
# setup.cfg
[[[[métadonnées]
Nom = structure_locale
version = 0,1.0
[[[[les options]
paquets = structure
# setup.py
importer setuptools
setuptools.installer()
En théorie, le Nom
et version
peut être ce que vous voulez. Cependant, ils seront utilisés par pépin
lorsque vous faites référence à votre package, vous devez donc choisir des valeurs reconnaissables et ne pas entrer en collision avec les autres packages que vous utilisez.
Une astuce consiste à donner à tous ces packages locaux un préfixe commun comme local_
ou votre nom d'utilisateur. paquets
devrait répertorier le ou les répertoires contenant votre code source. Vous pouvez ensuite installer le package localement en utilisant pépin
:
$ python -m pip install -e.
Cette commande installera le package sur votre système. structure
sera ensuite trouvé sur le chemin d'importation de Python, ce qui signifie que vous pouvez l'utiliser n'importe où sans avoir à vous soucier du répertoire de script, des importations relatives ou d'autres complications. le -e
option signifie modifiable, ce qui est important car il vous permet de modifier le code source de votre package sans le réinstaller.
Remarque: Ce type de fichier de configuration fonctionne très bien lorsque vous travaillez seul avec des projets. Cependant, si vous prévoyez de partager le code avec d'autres personnes, vous devez ajouter des informations supplémentaires à votre fichier d'installation.
Pour plus de détails sur les fichiers de configuration, consultez Comment publier un package Python Open-Source sur PyPI.
Maintenant que structure
est installé sur votre système, vous pouvez utiliser l'instruction d'importation suivante:
7 # Importations locales
8 de structure importer des dossiers
Cela fonctionnera, peu importe comment vous finirez par appeler votre application.
Pointe: Dans votre propre code, vous devez consciemment séparer les scripts et les bibliothèques. Voici une bonne règle d'or:
- UNE scénario est destiné à être exécuté.
- UNE bibliothèque est destiné à être importé.
Vous pouvez avoir du code que vous souhaitez à la fois exécuter seul et importer à partir d'autres scripts. Dans ce cas, il est généralement utile de refactoriser votre code afin de diviser la partie commune en un module de bibliothèque.
Bien que ce soit une bonne idée de séparer les scripts et les bibliothèques, tous les fichiers Python peuvent être à la fois exécutés et importés. Dans une section ultérieure, vous découvrirez comment créer des modules qui gèrent bien les deux.
Packages d'espace de noms
Les modules et packages Python sont très étroitement liés aux fichiers et répertoires. Cela distingue Python de nombreux autres langages de programmation dans lesquels les packages agissent simplement comme des espaces de noms sans imposer la façon dont le code source est organisé. Voir la discussion dans PEP 402 pour des exemples.
Packages d'espace de noms sont disponibles en Python depuis la version 3.3. Ceux-ci dépendent moins de la hiérarchie de fichiers sous-jacente. En particulier, les packages d'espace de noms peuvent être répartis sur plusieurs répertoires. Un package d'espace de noms est créé automatiquement si vous avez un répertoire contenant un .py
fichier mais pas __init__.py
. Voir PEP 420 pour une explication détaillée.
Remarque: Pour être précis, implicite Les packages d'espace de noms ont été introduits dans Python 3.3. Dans les versions antérieures de Python, vous pouviez créer manuellement des packages d'espace de noms de différentes manières incompatibles. Le PEP 420 unifie et simplifie ces approches antérieures.
Pour mieux comprendre pourquoi les packages d'espace de noms peuvent être utiles, essayons d'en implémenter un. À titre d'exemple motivant, vous aurez un autre essai sur le problème résolu dans Le modèle de méthode d'usine et son implémentation en Python: étant donné un Chanson
, vous souhaitez le convertir en l'une des représentations de chaîne. En d'autres termes, vous voulez sérialiser Chanson
objets.
Pour être plus concret, vous voulez implémenter du code qui fonctionne comme ceci:
>>> chanson = Chanson(song_id="1", Titre="La même rivière", artiste="Riverside")
>>> chanson.sérialiser()
'"id": "1", "title": "The Same River", "artist": "Riverside"' '
Supposons que vous avez de la chance et que vous rencontriez une implémentation tierce de plusieurs des formats que vous devez sérialiser, et elle est organisée comme un package d'espace de noms:
tierce personne/
│
└── sérialiseurs /
├── json.py
└── xml.py
Le fichier json.py
contient du code qui peut sérialiser un objet au format JSON:
# third_party / serializers / json.py
importer json
classe JsonSerializer:
def __init__(soi):
soi._current_object = Aucun
def start_object(soi, nom_objet, object_id):
soi._current_object = dicter(id=object_id)
def add_property(soi, Nom, valeur):
soi._current_object[[[[Nom] = valeur
def __str__(soi):
revenir json.décharges(soi._current_object)
Cette interface de sérialisation est un peu limitée, mais elle suffira à montrer comment fonctionnent les packages d'espace de noms.
Le fichier xml.py
contient un similaire XmlSerializer
qui peut convertir un objet en XML:
# third_party / serializers / xml.py
importer xml.etree.ElementTree comme et
classe XmlSerializer:
def __init__(soi):
soi._élément = Aucun
def start_object(soi, nom_objet, object_id):
soi._élément = et.Élément(nom_objet, attrib="id": object_id)
def add_property(soi, Nom, valeur):
soutenir = et.Sous-élément(soi._élément, Nom)
soutenir.texte = valeur
def __str__(soi):
revenir et.tostring(soi._élément, codage="unicode")
Notez que ces deux classes implémentent la même interface avec .start_object ()
, .add_property ()
, et .__ str __ ()
méthodes.
Vous créez ensuite un Chanson
classe qui peut utiliser ces sérialiseurs:
# song.py
classe Chanson:
def __init__(soi, song_id, Titre, artiste):
soi.song_id = song_id
soi.Titre = Titre
soi.artiste = artiste
def sérialiser(soi, sérialiseur):
sérialiseur.start_object("chanson", soi.song_id)
sérialiseur.add_property("Titre", soi.Titre)
sérialiseur.add_property("artiste", soi.artiste)
revenir str(sérialiseur)
UNE Chanson
est défini par son ID, son titre et son artiste. Notez que .serialize ()
n'a pas besoin de savoir dans quel format il se convertit, car il utilise l'interface commune définie précédemment.
En supposant que vous avez installé le tiers sérialiseurs
package, vous pouvez l'utiliser comme suit:
>>> de serializers.json importer JsonSerializer
>>> de serializers.xml importer XmlSerializer
>>> de chanson importer Chanson
>>> chanson = Chanson(song_id="1", Titre="La même rivière", artiste="Riverside")
>>> chanson.sérialiser(JsonSerializer())
'"id": "1", "title": "The Same River", "artist": "Riverside"' '
>>> chanson.sérialiser(XmlSerializer())
"La même rivière Riverside "
En fournissant différents objets sérialiseurs à .serialize ()
, vous obtenez différentes représentations de votre chanson.
Remarque: Vous pourriez obtenir un ModuleNotFoundError
ou un ImportError
lors de l'exécution du code vous-même. Ceci est dû au fait sérialiseurs
n'est pas dans votre chemin d'importation Python. Vous verrez bientôt comment résoudre ce problème.
Jusqu'ici tout va bien. Cependant, vous réalisez maintenant que vous devez également convertir vos chansons en une représentation YAML, qui n'est pas prise en charge dans la bibliothèque tierce. Entrez dans la magie des packages d'espace de noms: vous pouvez ajouter les vôtres YamlSerializer
à la sérialiseurs
package sans toucher à la bibliothèque tierce.
Tout d'abord, créez un répertoire sur votre système de fichiers local appelé sérialiseurs
. Il est important que le nom du répertoire corresponde au nom du package d'espace de noms que vous personnalisez:
local/
│
└── sérialiseurs /
└── yaml.py
dans le yaml.py
fichier, vous définissez votre propre YamlSerializer
. Vous basez cela sur le PyYAML
package, qui doit être installé à partir de PyPI:
$ python -m pip installe PyYAML
Étant donné que YAML et JSON sont des formats assez similaires, vous pouvez réutiliser la plupart des implémentations de JsonSerializer
:
# local / serializers / yaml.py
importer yaml
de serializers.json importer JsonSerializer
classe YamlSerializer(JsonSerializer):
def __str__(soi):
revenir yaml.déverser(soi._current_object)
Notez que le YamlSerializer
est basé sur le JsonSerializer
, qui est importé de sérialiseurs
lui-même. Depuis les deux json
et yaml
font partie du même package d'espace de noms, vous pouvez même utiliser une importation relative: de .json import JsonSerializer
.
En poursuivant l'exemple ci-dessus, vous pouvez maintenant convertir également le morceau en YAML:
>>> de serializers.yaml importer YamlSerializer
>>> chanson.sérialiser(YamlSerializer())
"artiste: Riverside nid: '1' ntitre: La même rivière n"
Tout comme les modules et packages normaux, les packages d'espace de noms doivent être trouvés sur le chemin d'importation Python. Si vous suiviez avec les exemples précédents, alors vous pourriez avoir eu des problèmes avec Python ne trouvant pas sérialiseurs
. Dans le code réel, vous auriez utilisé pépin
pour installer la bibliothèque tierce, elle serait donc automatiquement sur votre chemin.
Remarque: Dans l'exemple d'origine, le choix du sérialiseur a été fait de manière plus dynamique. Vous verrez comment utiliser les packages d'espace de noms dans un méthode d'usine modèle plus tard.
Vous devez également vous assurer que votre bibliothèque locale est disponible comme un package normal. Comme expliqué ci-dessus, vous pouvez le faire soit en exécutant Python à partir du répertoire approprié, soit en utilisant pépin
pour installer également la bibliothèque locale.
Dans cet exemple, vous testez comment intégrer un faux package tiers à votre package local. Si tierce personne
étaient un vrai paquet, alors vous le téléchargiez depuis PyPI en utilisant pépin
. Comme cela n'est pas possible, vous pouvez le simuler en installant tierce personne
localement comme vous l'avez fait dans le structure
exemple plus tôt.
Alternativement, vous pouvez jouer avec votre chemin d'importation. Mettez le tierce personne
et local
répertoires dans le même dossier, puis personnalisez votre chemin Python comme suit:
>>> importer sys
>>> sys.chemin.étendre([[[["tierce personne", "local"])
>>> de sérialiseurs importer json, xml, yaml
>>> json
>>> yaml
Vous pouvez désormais utiliser tous les sérialiseurs sans vous soucier de savoir s'ils sont définis dans le package tiers ou localement.
Guide de style des importations
PEP 8, le guide de style Python, contient quelques recommandations sur les importations. Comme toujours avec Python, garder votre code à la fois lisible et maintenable est une considération importante. Voici quelques règles générales pour définir le style de vos importations:
- Gardez les importations en haut du fichier.
- Écrivez les importations sur des lignes distinctes.
- Organisez les importations en groupes: d'abord les importations de bibliothèque standard, puis les importations tierces et enfin les importations d'applications ou de bibliothèques locales.
- L'ordre importe par ordre alphabétique dans chaque groupe.
- Préférez les importations absolues aux importations relatives.
- Évitez les importations de caractères génériques comme
depuis l'importation de module *
.
Je trie
et reorder-python-imports
sont d'excellents outils pour appliquer un style cohérent sur vos importations.
Voici un exemple de section d'importation dans le Vrai Python package lecteur de flux:
# Importations de bibliothèques standard
importer sys
de dactylographie importer Dict, liste
# Importations de tiers
importer analyseur
importer html2text
# Importations de lecteurs
de lecteur importer URL
Notez comment ce regroupement rend les dépendances de ce module claires: analyseur
et html2text
doivent être installés sur le système. You can generally assume that the standard library is available. Separating imports from within your package gives you some overview over the internal dependencies of your code.
There are cases in which it makes sense to bend these rules a little. You’ve already seen that relative imports can be an alternative to organizing package hierarchies. Later, you’ll see how in some cases you can move imports into a function definition to break import cycles.
Resource Imports
Sometimes you’ll have code that depends on data files or other resources. In small scripts, this isn’t a problem—you can specify the path to your data file and carry on!
However, if the resource file is important for your package and you want to distribute your package to other users, then a few challenges will arise:
-
You won’t have control over the path to the resource since that will depend on your user’s setup as well as on how the package is distributed and installed. You can try to figure out the resource path based on your package’s
__file__
ou__path__
attributes, but this may not always work as expected. -
Your package may reside inside a ZIP file or an old
.egg
file, in which case the resource won’t even be a physical file on the user’s system.
There have been several attempts at solving these challenges, including setuptools.pkg_resources
. However, with the introduction of importlib.resources
into the standard library in Python 3.7, there’s now one standard way of dealing with resource files.
Introducing importlib.resources
importlib.resources
gives access to resources within packages. In this context, a resource is any file located within an importable package. The file may or may not correspond to a physical file on the file system.
This has a couple of advantages. By reusing the import system, you get a more consistent way of dealing with the files inside your packages. It also gives you easier access to resource files in other packages. The documentation sums it up nicely:
If you can import a package, you can access resources within that package. (Source)
importlib.resources
became part of the standard library in Python 3.7. However, on older versions of Python, a backport is available as importlib_resources
. To use the backport, install it from PyPI:
$ python -m pip install importlib_resources
The backport is compatible with Python 2.7 as well as Python 3.4 and later versions.
There’s one requirement when using importlib.resources
: your resource files must be available inside a regular package. Namespace packages aren’t supported. In practice, this means that the file must be in a directory containing an __init__.py
fichier.
As a first example, assume you have resources inside a package like this:
books/
│
├── __init__.py
├── alice_in_wonderland.png
└── alice_in_wonderland.txt
__init__.py
is just an empty file necessary to designate livres
as a regular package.
You can then use open_text()
et open_binary()
to open text and binary files, respectively:
>>> de importlib import resources
>>> avec resources.open_text("books", "alice_in_wonderland.txt") comme fid:
... alice = fid.readlines()
...
>>> impression("".joindre(alice[:[:[:[:7]))
CHAPTER I. Down the Rabbit-Hole
Alice was beginning to get very tired of sitting by her sister on the
bank, and of having nothing to do: once or twice she had peeped into the
book her sister was reading, but it had no pictures or conversations in
it, 'and what is the use of a book,' thought Alice 'without pictures or
conversations?'
>>> avec resources.open_binary("books", "alice_in_wonderland.png") comme fid:
... cover = fid.read()
...
>>> cover[:[:[:[:8] # PNG file signature
b'x89PNGrnx1an'
open_text()
et open_binary()
are equivalent to the built-in open()
with the mode
parameter set to rt
et rb
, respectivement. Convenient functions for reading text or binary files directly are also available as read_text()
et read_binary()
. See the official documentation for more information.
Remarque: To seamlessly fall back to using the backport on older Python versions, you can import importlib.resources
as follows:
essayer:
de importlib import resources
sauf ImportError:
import importlib_resources comme resources
See the tips and tricks section of this tutorial for more information.
The rest of this section will show a few elaborate examples of using resource files in practice.
Example: Use Data Files
As a more complete example of using data files, you’ll see how to implement a quiz program based on United Nations population data. First, create a Les données
package and download WPP2019_TotalPopulationBySex.csv
from the UN web page:
data/
│
├── __init__.py
└── WPP2019_TotalPopulationBySex.csv
Open the CSV file and have a look at the data:
LocID,Location,VarID,Variant,Time,PopMale,PopFemale,PopTotal,PopDensity
4,Afghanistan,2,Medium,1950,4099.243,3652.874,7752.117,11.874
4,Afghanistan,2,Medium,1951,4134.756,3705.395,7840.151,12.009
4,Afghanistan,2,Medium,1952,4174.45,3761.546,7935.996,12.156
4,Afghanistan,2,Medium,1953,4218.336,3821.348,8039.684,12.315
...
Each line contains the population of a country for a given year and a given variant, which indicates what kind of scenario is used for the projection. The file contains population projections until the year 2100.
The following function reads this file and picks out the total population of each country for a given an
et variant
:
import csv
de importlib import resources
def read_population_file(an, variant="Medium"):
population =
impression(F"Reading population data for an, variant scenario")
avec resources.open_text(
"data", "WPP2019_TotalPopulationBySex.csv"
) comme fid:
rows = csv.DictReader(fid)
# Read data, filter the correct year
pour row dans rows:
si row[[[["Time"] == an et row[[[["Variant"] == variant:
pop = rond(float(row[[[["PopTotal"]) * 1000)
population[[[[row[[[["Location"]] = pop
return population
The highlighted lines show how importlib.resources
is used to open the data file. For more information about working with CSV files, check out Reading and Writing CSV Files in Python.
The above function returns a dictionary with population numbers:
>>> population = read_population_file("2020")
Reading population data for 2020, Medium scenario
>>> population[[[["Norway"]
5421242
You can do any number of interesting things with this population dictionary, including analysis and visualizations. Here, you’ll create a quiz game that asks users to identify which country in a set is most populous. Playing the game will look something like this:
$ python population_quiz.py
Question 1:
1. Tunisia
2. Djibouti
3. Belize
Which country has the largest population? 1
Yes, Tunisia is most populous (11,818,618)
Question 2:
1. Mozambique
2. Ghana
3. Hungary
Which country has the largest population? 2
No, Mozambique (31,255,435) is more populous than Ghana (31,072,945)
...
The details of the implementation are too far outside the topic of this tutorial, so they won’t be discussed here. However, you can expand the section below to see the complete source code.
The population quiz consists of two functions, one that reads the population data like you did above and one that runs the actual quiz:
1 # population_quiz.py
2
3 import csv
4 import Aléatoire
5
6 essayer:
7 de importlib import resources
8 sauf ImportError:
9 import importlib_resources comme resources
dix
11 def read_population_file(an, variant="Medium"):
12 """Read population data for the given year and variant"""
13 population =
14
15 impression(F"Reading population data for an, variant scenario")
16 avec resources.open_text(
17 "data", "WPP2019_TotalPopulationBySex.csv"
18 ) comme fid:
19 rows = csv.DictReader(fid)
20
21 # Read data, filter the correct year
22 pour row dans rows:
23 si (
24 int(row[[[["LocID"]) < 900
25 et row[[[["Time"] == an
26 et row[[[["Variant"] == variant
27 ):
28 pop = rond(float(row[[[["PopTotal"]) * 1000)
29 population[[[[row[[[["Location"]] = pop
30
31 return population
32
33 def run_quiz(population, num_questions, num_countries):
34 """Run a quiz about the population of countries"""
35 num_correct = 0
36 pour q_num dans range(num_questions):
37 impression(F"nnQuestion q_num + 1:")
38 des pays = Aléatoire.sample(population.keys(), num_countries)
39 impression(" n".joindre(F"je. une" pour je, une dans enumerate(des pays, start=1)))
40
41 # Get user input
42 while True:
43 guess_str = input(" nWhich country has the largest population? ")
44 essayer:
45 guess_idx = int(guess_str) - 1
46 guess = des pays[[[[guess_idx]
47 sauf (ValueError, IndexError):
48 impression(F"Please answer between 1 and num_countries")
49 else:
50 break
51
52 # Check the answer
53 correct = max(des pays, key=lambda k: population[[[[k])
54 si guess == correct:
55 num_correct += 1
56 impression(F"Yes, guess is most populous (population[[[[guess]:,)")
57 else:
58 impression(
59 F"No, correct (population[[[[correct]:,) is more populous "
60 F"than guess (population[[[[guess]:,)"
61 )
62
63 return num_correct
64
65 def main():
66 """Read population data and run quiz"""
67 population = read_population_file("2020")
68 num_correct = run_quiz(population, num_questions=dix, num_countries=3)
69 impression(F" nYou answered num_correct questions correctly")
70
71 si __name__ == "__main__":
72 main()
Note that on line 24, you also check that the LocID
is less than 900
. Locations with a LocID
de 900
and above are not proper countries, but aggregates like Monde
, Asie
, and so on.
Example: Add Icons to Tkinter GUIs
When building graphical user interfaces (GUIs), you often need to include resource files like icons. The following example shows how you can do that using importlib.resources
. The final app will look quite basic, but it’ll have a custom icon as well as an illustration on the Goodbye button:
The example uses Tkinter, which is a GUI package available in the standard library. It’s based on the Tk windowing system, originally developed for the Tcl programming language. There are many other GUI packages available for Python. If you’re using a different one, then you should be able add icons to your app using ideas similar to the ones presented here.
In Tkinter, images are handled by the PhotoImage
classe. To create a PhotoImage
, you pass in a path to an image file.
Remember, when distributing your package, you’re not even guaranteed that resource files will exist as physical files on the file system. importlib.resources
solves this by providing path()
. This function will return a path to the resource file, creating a temporary file if necessary.
To make sure any temporary files are cleaned up properly, you should use path()
as a context manager using the keyword avec
:
>>> de importlib import resources
>>> avec resources.path("hello_gui.gui_resources", "logo.png") comme path:
... impression(path)
...
/home/gahjelle/hello_gui/gui_resources/logo.png
For the full example, assume you have the following file hierarchy:
hello_gui/
│
├── gui_resources/
│ ├── __init__.py
│ ├── hand.png
│ └── logo.png
│
└── __main__.py
If you want to try the example yourself, then you can download these files along with the rest of the source code used in this tutorial by clicking the link below:
The code is stored in a file with the special name __main__.py
. This name indicates that the file is the entry point for the package. Having a __main__.py
file allows your package to be executed with python -m
:
For more information on calling a package with -m
, see How to Publish an Open-Source Python Package to PyPI.
The GUI is defined in a class called Bonjour
. Note that you use importlib.resources
to obtain the path of the image files:
1 # hello_gui/__main__.py
2
3 import tkinter comme tk
4 de tkinter import ttk
5
6 essayer:
7 de importlib import resources
8 sauf ImportError:
9 import importlib_resources comme resources
dix
11 class Bonjour(tk.Tk):
12 def __init__(self, *args, **kwargs):
13 super().__init__(*args, **kwargs)
14 self.wm_title("Hello")
15
16 # Read image, store a reference to it, and set it as an icon
17 avec resources.path("hello_gui.gui_resources", "logo.png") comme path:
18 self._icon = tk.PhotoImage(file=path)
19 self.iconphoto(True, self._icon)
20
21 # Read image, create a button, and store a reference to the image
22 avec resources.path("hello_gui.gui_resources", "hand.png") comme path:
23 hand = tk.PhotoImage(file=path)
24 bouton = ttk.Button(
25 self,
26 image=hand,
27 text="Goodbye",
28 command=self.quitter,
29 compound=tk.LEFT, # Add the image to the left of the text
30 )
31 bouton._image = hand
32 bouton.pack(côté=tk.TOP, padx=dix, pady=dix)
33
34 si __name__ == "__main__":
35 Bonjour = Bonjour()
36 Bonjour.mainloop()
If you want to learn more about building GUIs with Tkinter, then check out Python GUI Programming With Tkinter. The official documentation also has a nice list of resources to start with, and the tutorial at TkDocs is another great resource that shows how to use Tk in other languages.
Remarque: One source of confusion and frustration when working with images in Tkinter is that you must make sure the images aren’t garbage collected. Due to the way Python and Tk interact, the garbage collector in Python (at least in CPython) doesn’t register that images are used by .iconphoto()
et Button
.
To make sure that the images are kept around, you should manually add a reference to them. You can see examples of this in the code above on lines 18 and 31.
Dynamic Imports
One of Python’s defining features is that it’s a very dynamic language. Although it’s sometimes a bad idea, you can do many things to a Python program when it’s running, including adding attributes to a class, redefining methods, or changing the docstring of a module. For instance, you can change print()
so that it doesn’t do anything:
>>> impression("Hello dynamic world!")
Hello dynamic world!
>>> # Redefine the built-in print()
>>> impression = lambda *args, **kwargs: Aucun
>>> impression("Hush, everybody!")
>>> # Nothing is printed
Technically, you’re not redefining print()
. Instead, you’re defining un autre print()
that shadows the built-in one. To return to using the original print()
, you can delete your custom one with del print
. If you’re so inclined, you can shadow any Python object that is built into the interpreter.
Remarque: In the above example, you redefine print()
using a lambda function. You also could have used a normal function definition:
>>> def impression(*args, **kwargs):
... pass
To learn more about lambda functions, see How to Use Python Lambda Functions.
In this section, you’ll learn how to do dynamic imports in Python. With them, you won’t have to decide what to import until your program is running.
Using importlib
So far, you’ve used Python’s import
keyword to import modules and packages explicitly. However, the whole import machinery is available in the importlib
package, and this allows you to do your imports more dynamically. The following script asks the user for the name of a module, imports that module, and prints its docstring:
# docreader.py
import importlib
module_name = input("Name of module? ")
module = importlib.import_module(module_name)
impression(module.__doc__)
import_module()
returns a module object that you can bind to any variable. Then you can treat that variable as a regularly imported module. You can use the script like this:
$ python docreader.py
Name of module? math
This module is always available. It provides access to the
mathematical functions defined by the C standard.
$ python docreader.py
Name of module? csv
CSV parsing and writing.
This module provides classes that assist in the reading and writing
of Comma Separated Value (CSV) files, and implements the interface
described by PEP 305. Although many CSV files are simple to parse,
the format is not formally defined by a stable specification and
is subtle enough that parsing lines of a CSV file with something
like line.split(",") is bound to fail. The module supports three
basic APIs: reading, writing, and registration of dialects.
[...]
In each case, the module is imported dynamically by import_module()
.
Example: Factory Method With Namespace Packages
Think back to the serializers example from earlier. Avec serializers
implemented as a namespace package, you had the ability to add custom serializers. In the original example from a previous tutorial, the serializers were made available through a serializer factory. Using importlib
, you can do something similar.
Add the following code to your local serializers
namespace package:
# local/serializers/factory.py
import importlib
def get_serializer(format):
essayer:
module = importlib.import_module(F"serializers.format")
serializer = getattr(module, F"format.Titre()Serializer")
sauf (ImportError, AttributeError):
raise ValueError(F"Unknown format format!r") de Aucun
return serializer()
def serialize(serializable, format):
serializer = get_serializer(format)
serializable.serialize(serializer)
return str(serializer)
le get_serializer()
factory can create serializers dynamically based on the format
parameter, and serialize()
can then apply the serializer to any object that implements a .serialize()
method.
The factory makes some strong assumptions about the naming of both the module and the class containing the individual serializers. In the next section, you’ll learn about a plugin architecture that allows more flexibility.
You can now re-create the earlier example as follows:
>>> de serializers import factory
>>> de song import Song
>>> song = Song(song_id="1", Titre="The Same River", artist="Riverside")
>>> factory.serialize(song, "json")
'"id": "1", "title": "The Same River", "artist": "Riverside"'
>>> factory.serialize(song, "yaml")
"artist: Riverside, id: '1', title: The Same Rivern"
>>> factory.serialize(song, "toml")
ValueError: Unknown format 'toml'
In this case, you no longer need to explicitly import each serializer. Instead, you specify the name of a serializer with a string. The string could even be chosen by your user at runtime.
Remarque: In a regular package, you probably would have implemented get_serializer()
et serialize()
in an __init__.py
fichier. That would have allowed you to simply import serializers
and then call serializers.serialize()
.
However, namespace packages aren’t allowed to use __init__.py
, so you need to implement these functions in a separate module instead.
The final example shows that you also get a decent error message if you try to serialize to a format that hasn’t been implemented.
Example: A Package of Plugins
Let’s look at another example of using dynamic imports. You can use the following module to set up a flexible plugin architecture in your code. This is similar to the previous example, in which you could plug in serializers for different formats by adding new modules.
One application that uses plugins effectively is the Glue exploratory visualization tool. Glue can read many different data formats out of the box. However, if your data format isn’t supported, then you can write your own custom data loader.
You do this by adding a function that you decorate and place in a special location to make it easy for Glue to find. You don’t need to alter any part of the Glue source code. See the documentation for all the details.
You can set up a similar plugin architecture that you can use in your own projects. Within the architecture, there are two levels:
- A plugin package is a collection of related plugins corresponding to a Python package.
- A plugin is a custom behavior made available in a Python module.
le plugins
module that exposes the plugin architecture has the following functions:
# plugins.py
def register(func):
"""Decorator for registering a new plugin"""
def names(package):
"""List all plugins in one package"""
def avoir(package, brancher):
"""Get a given plugin"""
def call(package, brancher, *args, **kwargs):
"""Call the given plugin"""
def _import(package, brancher):
"""Import the given plugin file from a package"""
def _import_all(package):
"""Import all plugins in a package"""
def names_factory(package):
"""Create a names() function for one package"""
def get_factory(package):
"""Create a get() function for one package"""
def call_factory(package):
"""Create a call() function for one package"""
The factory functions are used to conveniently add functionality to plugin packages. You’ll see some examples of how they’re used shortly.
Looking at all the details of this code is outside the scope of this tutorial. If you’re interested, then you can see an implementation by expanding the section below.
The following code shows the implementation of plugins.py
described above:
# plugins.py
import functools
import importlib
de collections import namedtuple
de importlib import resources
# Basic structure for storing information about one plugin
Brancher = namedtuple("Plugin", ("name", "func"))
# Dictionary with information about all registered plugins
_PLUGINS =
def register(func):
"""Decorator for registering a new plugin"""
package, _, brancher = func.__module__.rpartition(".")
pkg_info = _PLUGINS.setdefault(package, )
pkg_info[[[[brancher] = Brancher(name=brancher, func=func)
return func
def names(package):
"""List all plugins in one package"""
_import_all(package)
return sorted(_PLUGINS[[[[package])
def avoir(package, brancher):
"""Get a given plugin"""
_import(package, brancher)
return _PLUGINS[[[[package][[[[brancher].func
def call(package, brancher, *args, **kwargs):
"""Call the given plugin"""
plugin_func = avoir(package, brancher)
return plugin_func(*args, **kwargs)
def _import(package, brancher):
"""Import the given plugin file from a package"""
importlib.import_module(F"package.brancher")
def _import_all(package):
"""Import all plugins in a package"""
files = resources.contents(package)
plugins = [[[[F[:[:[:[:-3] pour F dans files si F.endswith(".py") et F[[[[0] != "_"]
pour brancher dans plugins:
_import(package, brancher)
def names_factory(package):
"""Create a names() function for one package"""
return functools.partial(names, package)
def get_factory(package):
"""Create a get() function for one package"""
return functools.partial(avoir, package)
def call_factory(package):
"""Create a call() function for one package"""
return functools.partial(call, package)
This implementation is a bit simplified. In particular, it doesn’t do any explicit error handling. Check out the PyPlugs project for a more complete implementation.
You can see that _import()
uses importlib.import_module()
to dynamically load plugins. Additionally, _import_all()
uses importlib.resources.contents()
to list all available plugins in a given package.
Let’s look at some examples of how to use plugins. The first example is a greeter
package that you can use to add many different greetings to your app. A full plugin architecture is definitely overkill for this example, but it shows how the plugins work.
Assume you have the following greeter
package:
greeter/
│
├── __init__.py
├── hello.py
├── howdy.py
└── yo.py
Chaque greeter
module defines a function that takes one name
argument. Note how they’re all registered as plugins using the @register
decorator:
# greeter/hello.py
import plugins
@plugins.register
def greet(name):
impression(F"Hello name, how are you today?")
# greeter/howdy.py
import plugins
@plugins.register
def greet(name):
impression(F"Howdy good name, honored to meet you!")
# greeter/yo.py
import plugins
@plugins.register
def greet(name):
impression(F"Yo name, good times!")
To learn more about decorators and how they’re used, check out Primer on Python Decorators.
Remarque: To simplify the discovery and import of plugins, each plugin’s name is based on the name of the module that contains it instead of the function name. This restricts you to having only one plugin per file.
To finish setting up greeter
as a plugin package, you can use the factory functions in plugins
to add functionality to the greeter
package itself:
# greeter/__init__.py
import plugins
greetings = plugins.names_factory(__package__)
greet = plugins.call_factory(__package__)
You can now use greetings()
et greet()
as follows:
>>> import greeter
>>> greeter.greetings()
['hello', 'howdy', 'yo']
>>> greeter.greet(brancher="howdy", name="Guido")
Howdy good Guido, honored to meet you!
Notez que greetings()
automatically discovers all the plugins that are available in the package.
You can also more dynamically choose which plugin to call. In the following example, you choose a plugin at random. However, you could also select a plugin based on a configuration file or user input:
>>> import greeter
>>> import Aléatoire
>>> greeting = Aléatoire.choice(greeter.greetings())
>>> greeter.greet(greeting, name="Frida")
Hello Frida, how are you today?
>>> greeting = Aléatoire.choice(greeter.greetings())
>>> greeter.greet(greeting, name="Frida")
Yo Frida, good times!
To discover and call the different plugins, you need to import them. Let’s have a quick look at how plugins
handles imports. The main work is done in the following two functions inside plugins.py
:
import importlib
import pathlib
de importlib import resources
def _import(package, brancher):
"""Import the given plugin file from a package"""
importlib.import_module(F"package.brancher")
def _import_all(package):
"""Import all plugins in a package"""
files = resources.contents(package)
plugins = [[[[F[:[:[:[:-3] pour F dans files si F.endswith(".py") et F[[[[0] != "_"]
pour brancher dans plugins:
_import(package, brancher)
_import()
looks deceptively straightforward. It uses importlib
to import a module. But there are a couple of things also happening in the background:
- Python’s import system ensures that each plugin is imported only once.
@register
decorators defined inside each plugin module register each imported plugin.- In a full implementation, there would also be some error handling to deal with missing plugins.
_import_all()
discovers all the plugins within a package. Voici comment ça fonctionne:
contents()
deimportlib.resources
lists all the files inside a package.- The results are filtered to find potential plugins.
- Each Python file not starting with an underscore is imported.
- Plugins in any of the files are discovered and registered.
Let’s end this section with a final version of the serializers namespace package. One outstanding issue was that the get_serializer()
factory made strong assumptions about the naming of the serializer classes. You can make this more flexible using plugins.
First, add a line registering each of the serializers. Here is an example of how it’s done in the yaml
serializer:
# local/serializers/yaml.py
import plugins
import yaml
de serializers.json import JsonSerializer
@plugins.register
class YamlSerializer(JsonSerializer):
def __str__(self):
return yaml.dump(self._current_object)
Next, update get_serializers()
to use plugins
:
# local/serializers/factory.py
import plugins
get_serializer = plugins.call_factory(__package__)
def serialize(serializable, format):
serializer = get_serializer(format)
serializable.serialize(serializer)
return str(serializer)
You implement get_serializer()
en utilisant call_factory()
since that will automatically instantiate each serializer. With this refactoring, the serializers work just the same as earlier. However, you have more flexibility in naming your serializer classes.
For more information about using plugins, check out PyPlugs on PyPI and the Plug-ins: Adding Flexibility to Your Apps presentation from PyCon 2019.
The Python Import System
You’ve seen many ways to take advantage of Python’s import system. In this section, you’ll learn a bit more about what happens behind the scenes as modules and packages are imported.
As with most parts of Python, the import system can be customized. You’ll see several ways that you can change the import system, including automatically downloading missing packages from PyPI and importing data files as if they were modules.
Import Internals
The details of the Python import system are described in the official documentation. At a high level, three things happen when you import a module (or package). The module is:
- Searched for
- Loaded
- Bound to a namespace
For the usual imports—those done with the import
statement—all three steps happen automatically. When you use importlib
, however, only the first two steps are automatic. You need to bind the module to a variable or namespace yourself.
For instance, the following methods of importing and renaming math.pi
are roughly equivalent:
>>> de math import pi comme PI
>>> PI
3.141592653589793
>>> import importlib
>>> _tmp = importlib.import_module("math")
>>> PI = _tmp.pi
>>> del _tmp
>>> PI
3.141592653589793
Of course, in normal code you should prefer the former.
One thing to note is that, even when you import only one attribute from a module, the whole module is loaded and executed. The rest of the contents of the module just aren’t bound to the current namespace. One way to prove this is to have a look at what’s known as the module cache:
>>> de math import pi
>>> pi
3.141592653589793
>>> import sys
>>> sys.modules[[[["math"].cos(pi)
-1.0
sys.modules
acts as a module cache. It contains references to all modules that have been imported.
The module cache plays a very important role in the Python import system. The first place Python looks for modules when doing an import is in sys.modules
. If a module is already available, then it isn’t loaded again.
This is a great optimization, but it’s also a necessity. If modules were reloaded each time they were imported, then you could end up with inconsistencies in certain situations, such as when the underlying source code changes while a script is running.
Recall the import path you saw earlier. It essentially tells Python where to search for modules. However, if Python finds a module in the module cache, then it won’t bother searching the import path for the module.
Example: Singletons as Modules
In object-oriented programming, a singleton is a class with at most one instance. While it’s possible to implement singletons in Python, most good uses of singletons can be handled by modules instead. You can trust the module cache to instantiate a class only once.
As an example, let’s return to the United Nations population data you saw earlier. The following module defines a class wrapping the population data:
# population.py
import csv
de importlib import resources
import matplotlib.pyplot comme plt
class _Population:
def __init__(self):
"""Read the population file"""
self.Les données =
self.variant = "Medium"
impression(F"Reading population data for self.variant scenario")
avec resources.open_text(
"data", "WPP2019_TotalPopulationBySex.csv"
) comme fid:
rows = csv.DictReader(fid)
# Read data, filter the correct variant
pour row dans rows:
si int(row[[[["LocID"]) >= 900 ou row[[[["Variant"] != self.variant:
continue
pays = self.Les données.setdefault(row[[[["Location"], )
population = float(row[[[["PopTotal"]) * 1000
pays[[[[int(row[[[["Time"])] = rond(population)
def get_country(self, pays):
"""Get population data for one country"""
Les données = self.Les données[[[[pays]
années, population = Zip *: français(*Les données.articles())
return années, population
def plot_country(self, pays):
"""Plot data for one country, population in millions"""
années, population = self.get_country(pays)
plt.plot(années, [[[[p / 1e6 pour p dans population], étiquette=pays)
def order_countries(self, an):
"""Sort countries by population in decreasing order"""
des pays = c: self.Les données[[[[c][[[[an] pour c dans self.Les données
return sorted(des pays, key=lambda c: des pays[[[[c], reverse=True)
# Instantiate the Singleton
Les données = _Population()
Reading the data from disk takes some time. Since you don’t expect the data file to change, you instantiate the class when you load the module. The name of the class starts with an underscore to indicate to users that they shouldn’t use it.
Vous pouvez utiliser le population.data
singleton to create a Matplotlib graph showing the population projection for the most populous countries:
>>> import matplotlib.pyplot comme plt
>>> import population
Reading population data for Medium scenario
>>> # Pick out five most populous countries in 2050
>>> pour pays dans population.Les données.order_countries(2050)[:[:[:[:5]:
... population.Les données.plot_country(pays)
...
>>> plt.legend()
>>> plt.xlabel("Year")
>>> plt.ylabel("Population [Millions]")
>>> plt.Titre("UN Population Projections")
>>> plt.spectacle()
This creates a chart like the following:
Note that loading the data at import time is a kind of antipattern. Ideally, you want your imports to be as free of side effects as possible. A better approach would be to load the data lazily when you need it. You can do this quite elegantly using properties. Expand the following section to see an example.
The lazy implementation of population
stores the population data in ._data
the first time it’s read. le .data
property handles this caching of data:
# population.py
import csv
de importlib import resources
import matplotlib.pyplot comme plt
class _Population:
def __init__(self):
"""Prepare to read the population file"""
self._data =
self.variant = "Medium"
@property
def Les données(self):
"""Read data from disk"""
si self._data: # Data has already been read, return it directly
return self._data
# Read data and store it in self._data
impression(F"Reading population data for self.variant scenario")
avec resources.open_text(
"data", "WPP2019_TotalPopulationBySex.csv"
) comme fid:
rows = csv.DictReader(fid)
# Read data, filter the correct variant
pour row dans rows:
si int(row[[[["LocID"]) >= 900 ou row[[[["Variant"] != self.variant:
continue
pays = self._data.setdefault(row[[[["Location"], )
population = float(row[[[["PopTotal"]) * 1000
pays[[[[int(row[[[["Time"])] = rond(population)
return self._data
def get_country(self, pays):
"""Get population data for one country"""
pays = self.Les données[[[[pays]
années, population = Zip *: français(*pays.articles())
return années, population
def plot_country(self, pays):
"""Plot data for one country, population in millions"""
années, population = self.get_country(pays)
plt.plot(années, [[[[p / 1e6 pour p dans population], étiquette=pays)
def order_countries(self, an):
"""Sort countries by population in decreasing order"""
des pays = c: self.Les données[[[[c][[[[an] pour c dans self.Les données
return sorted(des pays, key=lambda c: des pays[[[[c], reverse=True)
# Instantiate the singleton
Les données = _Population()
Now the data won’t be loaded at import time. Instead, it’ll be imported the first time you access the _Population.data
dictionary. For more information about properties and the more general concept of descriptors, see Python Descriptors: An Introduction.
Reloading Modules
The module cache can be a little frustrating when you’re working in the interactive interpreter. It’s not trivial to reload a module after you change it. For example, take a look at the following module:
As part of testing and debugging this module, you import it in a Python console:
>>> import number
>>> number.answer
24
Let’s say you realize that you have a bug in your code, so you update the number.py
file in your editor:
Returning to your console, you import the updated module to see the effect of your fix:
>>> import number
>>> number.answer
24
Why is the answer still 24
? The module cache is doing its (now frustrating) magic: since Python imported number
earlier, it sees no reason to load the module again even though you just changed it.
The most straightforward solution to this is to exit the Python console and restart it. This forces Python to clear its module cache as well:
>>> import number
>>> number.answer
42
However, restarting the interpreter isn’t always feasible. You might be in a more complicated session that has taken you a long time to set up. If that’s the case, then you can use importlib.reload()
to reload a module instead:
>>> import number
>>> number.answer
24
>>> # Update number.py in your editor
>>> import importlib
>>> importlib.reload(number)
>>> number.answer
42
Notez que reload()
requires a module object, not a string like import_module()
does. Also, be aware that reload()
has some caveats. In particular, variables referring to objects within a module are not re-bound to new objects when that module is reloaded. See the documentation for more details.
Finders and Loaders
You saw earlier that creating modules with the same name as standard libraries can create problems. For example, if you have a file named math.py
in Python’s import path, then you won’t be able to import math
from the standard library.
This isn’t always the case, though. Create a file named time.py
with the following content:
# time.py
impression("Now's the time!")
Next, open a Python interpreter and import this new module:
>>> import temps
>>> # Nothing is printed
>>> temps.ctime()
'Mon Jun 15 14:26:12 2020'
>>> temps.tzname
('CET', 'CEST')
Something weird happened. It doesn’t seem like Python imported your new temps
module. Instead, it imported the temps
module from the standard library. Why are the standard library modules behaving inconsistently? You can get a hint by inspecting the modules:
>>> import math
>>> math
>>> import temps
>>> temps
You can see that math
is imported from a file, whereas temps
is some kind of built-in module. It seems that built-in modules aren’t shadowed by local ones.
Remarque: The built-in modules are compiled into the Python interpreter. Typically, they’re foundational modules like builtins
, sys
, et temps
. Which modules are built in depends on your Python interpreter, but you can find their names in sys.builtin_module_names
.
Let’s dig even deeper into Python’s import system. This will also show why built-in modules aren’t shadowed by local ones. There are several steps involved when importing a module:
-
Python checks if the module is available in the module cache. Si
sys.modules
contains the name of the module, then the module is already available, and the import process ends. -
Python starts looking for the module using several finders. A finder will search for the module using a given strategy. The default finders can import built-in modules, frozen modules, and modules on the import path.
-
Python loads the module using a loader. Which loader Python uses is determined by the finder that located the module and is specified in something called a module spec.
You can extend the Python import system by implementing your own finder and, if necessary, your own loader. You’ll see a more useful example of a finder later. For now, you’ll learn how to do basic (and possibly silly) customizations of the import system.
sys.meta_path
controls which finders are called during the import process:
>>> import sys
>>> sys.meta_path
[[[[,
,
]
First, note that this answers the question from earlier: built-in modules aren’t shadowed by local modules because the built-in finder is called before the import path finder, which finds local modules. Second, note that you can customize sys.meta_path
to your liking.
To quickly mess up your Python session, you can remove all finders:
>>> import sys
>>> sys.meta_path.clair()
>>> sys.meta_path
[]
>>> import math
Traceback (most recent call last):
Fichier "" , line 1, in
ModuleNotFoundError: No module named 'math'
>>> import importlib # Autoimported at start-up, still in the module cache
>>> importlib
Since there are no finders, Python can’t find or import new modules. However, Python can still import modules that are already in the module cache since it looks there before calling any finders.
In the example above, importlib
was already loaded under the hood before you cleared the list of finders. If you really want to make your Python session completely unusable, then you can also clear the module cache, sys.modules
.
The following is a slightly more useful example. You’ll write a finder that prints a message to the console identifying the module being imported. The example shows how to add your own finder, although it doesn’t actually attempt to find a module:
1 # debug_importer.py
2
3 import sys
4
5 class DebugFinder:
6 @classmethod
7 def find_spec(cls, name, path, target=Aucun):
8 impression(F"Importing name!r")
9 return Aucun
dix
11 sys.meta_path.insert(0, DebugFinder)
All finders must implement a .find_spec()
class method, which should try to find a given module. There are three ways that .find_spec()
can terminate:
- Par returning
Aucun
if it doesn’t know how to find and load the module - Par returning a module spec specifying how to load the module
- Par raising a
ModuleNotFoundError
to indicate that the module can’t be imported
le DebugFinder
prints a message to the console and then explicitly returns Aucun
to indicate that other finders should figure out how to actually import the module.
Remarque: Since Python implicitly Retour Aucun
from any function or method without an explicit return
, you can leave out line 9. However, in this case it’s good to include return None
to make it clear that DebugFinder
doesn’t find a module.
By inserting DebugFinder
first in the list of finders, you get a running list of all modules being imported:
>>> import debug_importer
>>> import csv
Importing 'csv'
Importing 're'
Importing 'enum'
Importing 'sre_compile'
Importing '_sre'
Importing 'sre_parse'
Importing 'sre_constants'
Importing 'copyreg'
Importing '_csv'
You can, for instance, see that importing csv
triggers the import of several other modules that csv
depends on. Note that the verbose option to the Python interpreter, python -v
, gives the same information and much, much more.
For another example, say that you’re on a quest to rid the world of regular expressions. (Now, why would you want such a thing? Regular expressions are great!) You could implement the following finder that bans the re
regular expressions module:
# ban_importer.py
import sys
BANNED_MODULES = "re"
class BanFinder:
@classmethod
def find_spec(cls, name, path, target=Aucun):
si name dans BANNED_MODULES:
raise ModuleNotFoundError(F"name!r is banned")
sys.meta_path.insert(0, BanFinder)
Raising a ModuleNotFoundError
ensures that no finder later in the list of finders will be executed. This effectively stops you from using regular expressions in Python:
>>> import ban_importer
>>> import csv
Traceback (most recent call last):
Fichier "" , line 1, in
Fichier ".../python/lib/python3.8/csv.py", line 6, in
import re
Fichier "ban_importer.py", line 11, in find_spec
raise ModuleNotFoundError(F"name!r is banned")
ModuleNotFoundError: 're' is banned
Even though you’re importing only csv
, that module is importing re
behind the scenes, so an error is raised.
Example: Automatically Install From PyPI
Because the Python import system is already quite powerful and useful, there are many more ways to mess it up than there are to extend it in a useful way. However, the following example can be useful in certain situations.
The Python Package Index (PyPI) is your one-stop cheese shop for finding third-party modules and packages. It’s also the place from which pip
downloads packages.
En d'autre Real Python tutorials, you may have seen instructions to use python -m pip install
to install the third-party modules and packages you need for following along with examples. Wouldn’t it be great to have Python automatically install missing modules for you?
Avertissement: In most cases, it really wouldn’t be great to have Python install modules automatically. For instance, in most production settings you want to stay in control of your environment. Furthermore, the documentation cautions against using pip
this way.
To avoid messing up your Python installation, you should play with this code only in environments that you wouldn’t mind deleting or reinstalling.
The following finder attempts to install modules using pip
:
# pip_importer.py
de importlib import util
import subprocess
import sys
class PipFinder:
@classmethod
def find_spec(cls, name, path, target=Aucun):
impression(F"Module name!r not installed. Attempting to pip install")
cmd = F"sys.executable -m pip install name"
essayer:
subprocess.run(cmd.split(), check=True)
sauf subprocess.CalledProcessError:
return Aucun
return util.find_spec(name)
sys.meta_path.append(PipFinder)
Compared to the finders you saw earlier, this one is slightly more complicated. By putting this finder last in the list of finders, you know that if you call PipFinder
, then the module won’t be found on your system. The job of .find_spec()
is therefore just to do the pip install
. If the installation works, then the module spec will be created and returned.
Try to use the parse
library without installing it yourself:
>>> import pip_importer
>>> import parse
Module 'parse' not installed. Attempting to pip install
Collecting parse
Downloading parse-1.15.0.tar.gz (29 kB)
Building wheels for collected packages: parse
Building wheel for parse (setup.py) ... done
Successfully built parse
Installing collected packages: parse
Successfully installed parse-1.15.0
>>> pattern = "my name is name"
>>> parse.parse(pattern, "My name is Geir Arne")
Normally, import parse
would’ve raised a ModuleNotFoundError
, but in this case parse
is installed and imported.
Tandis que le PipFinder
seemingly works, there are some challenges with this approach. One major problem is that the import name of a module doesn’t always correspond to its name on PyPI. For example, the Real Python feed reader is called realpython-reader
on PyPI, but the import name is simply reader
.
Using PipFinder
to import and install reader
ends up installing the wrong package:
>>> import pip_importer
>>> import reader
Module 'reader' not installed. Attempting to pip install
Collecting reader
Downloading reader-1.2-py3-none-any.whl (68 kB)
...
This could have disastrous consequences for your project.
One situation in which automatic installations can be quite helpful is when you’re running Python in the cloud with more limited control over your environment, such as when you’re running Jupyter-style notebooks at Google Colaboratory. The Colab notebook environment is great for doing cooperative data exploration.
A typical notebook comes with many data science packages installed, including NumPy, Pandas, and Matplotlib, and you can add new packages with pip
. But you can also activate automatic installation:
Since pip_importer
isn’t available locally on the Colab server, the code is copied into the first cell of the notebook.
Example: Import Data Files
The final example in this section is inspired by Aleksey Bilogur’s great blog post Import Almost Anything in Python: An Intro to Module Loaders and Finders. You’ve already seen how to use importlib.resources
to import datafiles. Here, you’ll instead implement a custom loader that can import a CSV file directly.
Earlier, you worked with a huge CSV file with population data. To make the custom loader example more manageable, consider the following smaller employees.csv
fichier:
name,department,birthday month
John Smith,Accounting,November
Erica Meyers,IT,March
The first line is a header naming three fields, and the following two rows of data each contain information about an employee. For more information about working with CSV files, check out Reading and Writing CSV Files in Python.
Your goal in this section is to write a finder and a loader that allow you to import the CSV file directly so that you can write code like the following:
>>> import csv_importer
>>> import employés
>>> employés.name
('John Smith', 'Erica Meyers')
>>> pour row dans employés.Les données:
... impression(row[[[["department"])
...
Comptabilité
IL
>>> pour name, mois dans Zip *: français(employés.name, employés.birthday_month):
... impression(F"name is born in mois")
...
John Smith is born in November
Erica Meyers is born in March
>>> employés.__file__
'employees.csv'
The job of the finder will be to search for and recognize CSV files. The loader’s job will be to import the CSV data. Often, you can implement finders and corresponding loaders in one common class. That’s the approach you’ll take here:
1 # csv_importer.py
2
3 import csv
4 import pathlib
5 import re
6 import sys
7 de importlib.machinery import ModuleSpec
8
9 class CsvImporter():
dix def __init__(self, csv_path):
11 """Store path to CSV file"""
12 self.csv_path = csv_path
13
14 @classmethod
15 def find_spec(cls, name, path, target=Aucun):
16 """Look for CSV file"""
17 package, _, module_name = name.rpartition(".")
18 csv_file_name = F"module_name.csv"
19 directories = sys.path si path est Aucun else path
20 pour annuaire dans directories:
21 csv_path = pathlib.Path(annuaire) / csv_file_name
22 si csv_path.exists():
23 return ModuleSpec(name, cls(csv_path))
24
25 def create_module(self, spec):
26 """Returning None uses the standard machinery for creating modules"""
27 return Aucun
28
29 def exec_module(self, module):
30 """Executing the module means reading the CSV file"""
31 # Read CSV data and store as a list of rows
32 avec self.csv_path.ouvert() comme fid:
33 rows = csv.DictReader(fid)
34 Les données = list(rows)
35 fieldnames = tuple(_identifier(F) pour F dans rows.fieldnames)
36
37 # Create a dict with each field
38 valeurs = Zip *: français(*(row.valeurs() pour row dans Les données))
39 fields = dict(Zip *: français(fieldnames, valeurs))
40
41 # Add the data to the module
42 module.__dict__.mettre à jour(fields)
43 module.__dict__[[[["data"] = Les données
44 module.__dict__[[[["fieldnames"] = fieldnames
45 module.__file__ = str(self.csv_path)
46
47 def __repr__(self):
48 """Nice representation of the class"""
49 return F"self.__class__.__name__(str(self.csv_path)!r)"
50
51 def _identifier(var_str):
52 """Create a valid identifier from a string
53
54 See https://stackoverflow.com/a/3305731
55 """
56 return re.sous(r"W|^(?=d)", "_", var_str)
57
58 # Add the CSV importer at the end of the list of finders
59 sys.meta_path.append(CsvImporter)
There’s quite a bit of code in this example! Luckily, most of the work is done in .find_spec()
et .exec_module()
. Let’s look at them in more detail.
As you saw earlier, .find_spec()
is responsible for finding the module. In this case, you’re looking for CSV files, so you create a filename with a .csv
suffix. name
contains the full name of the module that is imported. For example, if you use from data import employees
, puis name
sera data.employees
. In this case, the filename will be employees.csv
.
For top-level imports, path
sera Aucun
. In that case, you look for the CSV file in the full import path, which will include the current working directory. If you’re importing a CSV file within a package, then path
will be set to the path or paths of the package. If you find a matching CSV file, then a module spec is returned. This module spec tells Python to load the module using CsvImporter
.
The CSV data is loaded by .exec_module()
. Vous pouvez utiliser csv.DictReader
from the standard library to do the actual parsing of the file. Like most things in Python, modules are backed by dictionaries. By adding the CSV data to module.__dict__
, you make it available as attributes of the module.
For instance, adding fieldnames
to the module dictionary on line 44 allows you to list the field names in the CSV file as follows:
>>> employés.fieldnames
('name', 'department', 'birthday_month')
In general, CSV field names can contain spaces and other characters that aren’t allowed in Python attribute names. Before adding the fields as attributes on the module, you sanitize the field names using a regular expression. This is done in _identifier()
starting on line 51.
You can see an example of this effect in the birthday_month
field name above. If you look at the original CSV file, then you’ll see that the header says birthday month
with a space instead of an underscore.
By hooking this CsvImporter
into the Python import system, you get a fair bit of functionality for free. For example, the module cache will make sure that the data file is loaded only once.
Import Tips and Tricks
To round out this tutorial, you’ll see a few tips about how to handle certain situations that come up from time to time. You’ll see how to deal with missing packages, cyclical imports, and even packages stored inside ZIP files.
Handle Packages Across Python Versions
Sometimes you need to deal with packages that have different names depending on the Python version. You’ve already seen one example of this: importlib.resources
has only been available since Python 3.7. In earlier versions of Python, you need to install and use importlib_resources
au lieu.
As long as the different versions of the package are compatible, you can handle this by renaming the package with comme
:
essayer:
de importlib import resources
sauf ImportError:
import importlib_resources comme resources
In the rest of the code, you can refer to resources
and not worry about whether you’re using importlib.resources
ou importlib_resources
.
Normally, it’s easiest to use a try...except
statement to figure out which version to use. Another option is to inspect the version of the Python interpreter. However, this may add some maintenance cost if you need to update the version numbers.
You could rewrite the previous example as follows:
import sys
si sys.version_info >= (3, 7):
de importlib import resources
else:
import importlib_resources comme resources
This would use importlib.resources
on Python 3.7 and newer while falling back to importlib_resources
on older versions of Python. Voir le flake8-2020
project for good and future-proof advice on how to check which Python version is running.
Handle Missing Packages: Use an Alternative
The following use case is closely related to the previous example. Assume there’s a compatible reimplementation of a package. The reimplementation is better optimized, so you want to use it if it’s available. However, the original package is more easily available and also delivers acceptable performance.
One such example is quicktions
, which is an optimized version of fractions
from the standard library. You can handle these preferences the same way you handled different package names earlier:
essayer:
de quicktions import Fraction
sauf ImportError:
de fractions import Fraction
This will use quicktions
if it’s available and fall back to fractions
if not.
Another similar example is the UltraJSON package, an ultrafast JSON encoder and decoder that can be used as a replacement for json
in the standard library:
essayer:
import ujson comme json
sauf ImportError:
import json
By renaming ujson
à json
, you don’t have to worry about which package was actually imported.
Handle Missing Packages: Use a Mock Instead
A third, related example is adding a package that provides a nice-to-have feature that’s not strictly necessary for your app. Again, this can be solved by adding try...except
to your imports. The extra challenge is how you will replace the optional package if it’s not available.
For a concrete example, say that you’re using Colorama to add colored text in the console. Colorama mainly consists of special string constants that add color when printed:
>>> import colorama
>>> colorama.init(autoreset=True)
>>> de colorama import Arrière, Fore
>>> Fore.RED
'x1b[31m'[31m'[31m'[31m'
>>> impression(F"Fore.REDHello Color!")
Hello Color!
>>> impression(F"Arrière.REDHello Color!")
Hello Color!
Unfortunately, the color doesn’t render in the example above. In your terminal it’ll look something like this:
Before you start using Colorama colors, you should call colorama.init()
. Setting autoreset
à True
means that the color directives will be automatically reset at the end of the string. It’s a useful setting if you want to color just one line at a time.
If you’d rather have tout your output be (for example) blue, then you can let autoreset
être False
and add Fore.BLUE
to the beginning of your script. The following colors are available:
>>> de colorama import Fore
>>> sorted(c pour c dans dir(Fore) si ne pas c.startswith("_"))
['BLACK''BLUE''CYAN''GREEN''LIGHTBLACK_EX''LIGHTBLUE_EX'['BLACK''BLUE''CYAN''GREEN''LIGHTBLACK_EX''LIGHTBLUE_EX'['BLACK''BLUE''CYAN''GREEN''LIGHTBLACK_EX''LIGHTBLUE_EX'['BLACK''BLUE''CYAN''GREEN''LIGHTBLACK_EX''LIGHTBLUE_EX'
'LIGHTCYAN_EX', 'LIGHTGREEN_EX', 'LIGHTMAGENTA_EX', 'LIGHTRED_EX',
'LIGHTWHITE_EX', 'LIGHTYELLOW_EX', 'MAGENTA', 'RED', 'RESET',
'WHITE', 'YELLOW']
You can also use colorama.Style
to control the style of your text. You can choose between DIM
, NORMAL
, et BRIGHT
.
Finalement, colorama.Cursor
provides codes for controlling the position of the cursor. You can use it to display the progress or status of a running script. The following example displays a countdown from dix
:
# countdown.py
import colorama
de colorama import Cursor, Fore
import temps
colorama.init(autoreset=True)
compte à rebours = [[[[F"Fore.BLUEn" pour n dans range(dix, 0, -1)]
compte à rebours.append(F"Fore.REDLift off!")
impression(F"Fore.GREENCountdown starting: n")
pour compter dans compte à rebours:
temps.sleep(1)
impression(F"Cursor.UP(1)compter ")
Note how the counter stays in place instead of printing on separate lines as it normally would:
Let’s get back to the task at hand. For many applications, adding color to your console output is cool but not critical. To avoid adding yet another dependency to your app, you want to use Colorama only if it’s available on the system and not break the app if it isn’t.
To do this, you can take inspiration from testing and its use of mocks. A mock can substitute for another object while allowing you to control its behavior. Here’s a naïve attempt at mocking Colorama:
>>> de unittest.mock import Mock
>>> colorama = Mock()
>>> colorama.init(autoreset=True)
>>> Fore = Mock()
>>> Fore.RED
>>> impression(F"Fore.REDHello Color!")
Hello Color!
This doesn’t quite work, because Fore.RED
is represented by a string that messes up your output. Instead, you want to create an object that always renders as the empty string.
It’s possible to change the return value of .__str__()
sur Mock
objects. However, in this case, it’s more convenient to write your own mock:
# optional_color.py
essayer:
de colorama import init, Arrière, Cursor, Fore, Style
sauf ImportError:
de collections import UserString
class ColoramaMock(UserString):
def __call__(self, *args, **kwargs):
return self
def __getattr__(self, key):
return self
init = ColoramaMock("")
Arrière = Cursor = Fore = Style = ColoramaMock("")
ColoramaMock("")
is an empty string that will also return the empty string when it’s called. This effectively gives us a reimplementation of Colorama, just without the colors.
The final trick is that .__getattr__()
returns itself, so that all colors, styles, and cursor movements that are attributes on Arrière
, Fore
, Style
, et Cursor
are mocked as well.
le optional_color
module is designed to be a drop-in replacement for Colorama, so you can update the countdown example using search and replace:
# countdown.py
import optional_color
de optional_color import Cursor, Fore
import temps
optional_color.init(autoreset=True)
compte à rebours = [[[[F"Fore.BLUEn" pour n dans range(dix, 0, -1)]
compte à rebours.append(F"Fore.REDLift off!")
impression(F"Fore.GREENCountdown starting: n")
pour compter dans compte à rebours:
temps.sleep(1)
impression(F"Cursor.UP(1)compter ")
If you run this script on a system in which Colorama isn’t available, then it’ll still work, but it may not look as nice:
With Colorama installed, you should see the same results as earlier.
Import Scripts as Modules
One difference between scripts and library modules is that scripts typically do something, whereas libraries provide functionality. Both scripts and libraries live inside regular Python files, and as far as Python is concerned, there’s no difference between them.
Instead, the difference is in how the file is meant to be used: should it be executed with python file.py
or imported with import file
inside another script?
Sometimes you’ll have a module that works as both a script and a library. You could try to refactor your module into two different files.
One example of this in the standard library is the json
package. You usually use it as a library, but it also comes bundled with a script that can prettify JSON files. Assume you have the following colors.json
fichier:
{"colors": [{[{[["color": "blue", "category": "hue", "type": "primary",
"code": "rgba": [[[[0,0,255,1], "hex": "#00F", "color": "yellow",
"category": "hue", "type": "primary", "code": "rgba": [[[[255,255,0,1],
"hex": "#FF0"]
As JSON is often read only by machines, many JSON files aren’t formatted in a readable fashion. In fact, it’s quite common for JSON files to consist of one very long line of text.
json.tool
is a script that uses the json
library to format JSON in a more readable fashion:
$ python -m json.tool colors.json --sort-keys
"colors":[[[[
"category": "hue",
"code":
"hex": "#00F",
"rgba":[[[[
0,
0,
255,
1
]
,
"color": "blue",
"type": "primary"
,
"category": "hue",
"code":
"hex": "#FF0",
"rgba":[[[[
255,
255,
0,
1
]
,
"color": "yellow",
"type": "primary"
]
Now the structure of the JSON file becomes much less complicated to grasp. Vous pouvez utiliser le --sort-keys
option to sort keys alphabetically.
While it’s good practice to split scripts and libraries, Python has an idiom that makes it possible to treat a module as both a script and a library at the same time. As noted earlier, the value of the special __name__
module variable is set at runtime based on whether the module is imported or run as a script.
Let’s test it out! Create the following file:
# name.py
impression(__name__)
If you run this file, then you’ll see that __name__
is set to the special value __main__
:
$ python name.py
__main__
However, if you import the module, then __name__
is set to the name of the module:
This behavior is leveraged in the following pattern:
def main():
...
si __name__ == "__main__":
main()
Let’s use this in a bigger example. In an attempt to keep you young, the following script will replace any “old” age (25
or above) with 24
:
1 # feel_young.py
2
3 def make_young(text):
4 words = [[[[replace_by_age(w) pour w dans text.split()]
5 return " ".joindre(words)
6
7 def replace_by_age(word, new_age=24, age_range=(25, 120)):
8 si word.isdigit() et int(word) dans range(*age_range):
9 return str(new_age)
dix return word
11
12 si __name__ == "__main__":
13 text = input("Tell me something: ")
14 impression(make_young(text))
You can run this as a script, and it will interactively make the age you type younger:
$ python feel_young.py
Tell me something: Forever young - Bob is 79 years old
Forever young - Bob is 24 years old
You can also use the module as an importable library. le si
test on line 12 makes sure that there are no side effects when you import the library. Only the functions make_young()
et replace_by_age()
are defined. You can, for instance, use this library as follows:
>>> de feel_young import make_young
>>> headline = "Twice As Many 100-Year-Olds"
>>> make_young(headline)
'Twice As Many 24-Year-Olds'
Without the protection of the si
test, the import would have triggered the interactive input()
and made feel_young
very hard to use as a library.
Run Python Scripts From ZIP Files
A slightly obscure feature of Python is that it can run scripts packaged into ZIP files. The main advantage of this is that you can distribute a full package as a single file.
Note, however, that this still requires Python to be installed on the system. If you want to distribute your Python application as a stand-alone executable file, then see Using PyInstaller to Easily Distribute Python Applications.
If you give the Python interpreter a ZIP file, then it’ll look for a file named __main__.py
inside the ZIP archive, extract it, and run it. As a basic example, create the following __main__.py
fichier:
# __main__.py
impression(F"Hello from __file__")
This will print a message when you run it:
$ python __main__.py
Hello from __main__.py
Now add it to a ZIP archive. You may be able to do this on the command line:
$ zip hello.zip __main__.py
adding: __main__.py (stored 0%)
On Windows, you can instead use point and click. Select the file in the File Explorer, then right-click and select Send to → Compressed (zipped) folder.
Since __main__
isn’t a very descriptive name, you named the ZIP file hello.zip
. You can now call it directly with Python:
$ python hello.zip
Hello from hello.zip/__main__.py
Note that your script is aware that it lives inside hello.zip
. Furthermore, the root of your ZIP file is added to Python’s import path so that your scripts can import other modules inside the same ZIP file.
Think back to the earlier example in which you created a quiz based on population data. It’s possible to distribute this whole application as a single ZIP file. importlib.resources
will make sure the data file is extracted from the ZIP archive when it’s needed.
The app consists of the following files:
population_quiz/
│
├── data/
│ ├── __init__.py
│ └── WPP2019_TotalPopulationBySex.csv
│
└── population_quiz.py
You could add these to a ZIP file in the same way you did above. However, Python comes with a tool called zipapp
that streamlines the process of packing applications into ZIP archives. You use it as follows:
$ python -m zipapp population_quiz -m population_quiz:main
This command essentially does two things: it creates an entry point and packages your application.
Remember that you needed a __main__.py
file as an entry point inside your ZIP archive. If you supply the -m
option with information about how your app should be started, then zipapp
creates this file for you. In this example, the generated __main__.py
looks like this:
# -*- coding: utf-8 -*-
import population_quiz
population_quiz.main()
Ce __main__.py
is packaged, along with the contents of the population_quiz
directory, into a ZIP archive named population_quiz.pyz
. le .pyz
suffix signals that this is a Python file wrapped into a ZIP archive.
Remarque: By default, zipapp
doesn’t compress any files. It only packages them into a single file. You can tell zipapp
to compress the files as well by adding the -c
option.
However, this feature is available only in Python 3.7 and later. Voir le zipapp
documentation for more information.
On Windows, .pyz
files should already be registered as Python files. On Mac and Linux, you can have zipapp
create executable files by using the -p
interpreter option and specifying which interpreter to use:
$ python -m zipapp population_quiz -m population_quiz:main
> -p "/usr/bin/env python"
le -p
option adds a shebang (#!
) that tells the operating system how to run the file. Additionally, it makes the .pyz
file executable so that you can run the file just by typing its name:
$ ./population_quiz.pyz
Reading population data for 2020, Medium scenario
Question 1:
1. Timor-Leste
2. Viet Nam
3. Bermuda
Which country has the largest population?
Notice the ./
in front of the filename. This is a typical trick on Mac and Linux to run executable files in the current directory. If you move the file to a directory on your PATH
, or if you’re using Windows, then you should be able to use only the filename: population_quiz.pyz
.
Remarque: On Python 3.6 and older, the previous command will fail with a message saying that it couldn’t find the population data resource in the Les données
annuaire. This is due to a limitation in zipimport
.
A workaround is to supply the absolute path to population_quiz.pyz
. On Mac and Linux, you can do this with the following trick:
$ "pwd"/population_quiz.pyz
le pwd
command expands to the path of the current directory.
Let’s close this section by looking at a nice effect of using importlib.resources
. Remember that you used the following code to open the data file:
de importlib import resources
avec resources.open_text("data", "WPP2019_TotalPopulationBySex.csv") comme fid:
...
A more common way to open data files is to locate them based on your module’s __file__
attribute:
import pathlib
DATA_DIR = pathlib.Path(__file__).parent / "data"
avec ouvert(DATA_DIR / "WPP2019_TotalPopulationBySex.csv") comme fid:
...
This approach usually works well. However, it falls apart when your application is packed into a ZIP file:
$ python population_quiz.pyz
Reading population data for 2020, Medium scenario
Traceback (most recent call last):
...
NotADirectoryError: 'population_quiz.pyz/data/WPP2019_TotalPopulationBySex.csv'
Your data file is inside the ZIP archive, so open()
isn’t able to open it. importlib.resources
, on the other hand, will extract your data to a temporary file before opening it.
Handle Cyclical Imports
A cyclical import happens when you have two or more modules importing each other. More concretely, imagine that the module yin
uses import yang
and the module yang
similarly imports yin
.
Python’s import system is to some extent designed to handle import cycles. For instance, the following code—while not very useful—runs fine:
# yin.py
impression(F"Hello from yin")
import yang
impression(F"Goodbye from yin")
# yang.py
impression(F"Hello from yang")
import yin
impression(F"Goodbye from yang")
Trying to import yin
in the interactive interpreter imports yang
as well:
>>> import yin
Hello from yin
Hello from yang
Goodbye from yang
Goodbye from yin
Notez que yang
is imported in the middle of the import of yin
, precisely at the import yang
statement in the source code of yin
. The reason this doesn’t end up in endless recursion is our old friend the module cache.
When you type import yin
, a reference to yin
is added to the module cache even before yin
is loaded. Quand yang
tries to import yin
later, it simply uses the reference in the module cache.
You can also have modules that do something slightly more useful. If you define attributes and functions in your modules, then it all still works:
# yin.py
impression(F"Hello from yin")
import yang
number = 42
def combiner():
return number + yang.number
impression(F"Goodbye from yin")
# yang.py
impression(F"Hello from yang")
import yin
number = 24
def combiner():
return number + yin.number
impression(F"Goodbye from yang")
Importation yin
works the same as before:
>>> import yin
Hello from yin
Hello from yang
Goodbye from yang
Goodbye from yin
The issues associated with recursive imports start popping up when you actually use the other module at import time instead of just defining functions that will use the other module later. Add one line to yang.py
:
# yin.py
impression(F"Hello from yin")
import yang
number = 42
def combiner():
return number + yang.number
impression(F"Goodbye from yin")
# yang.py
impression(F"Hello from yang")
import yin
number = 24
def combiner():
return number + yin.number
impression(F"yin and yang combined is combiner()")
impression(F"Goodbye from yang")
Now Python gets confused by the import:
>>> import yin
Hello from yin
Hello from yang
Traceback (most recent call last):
...
Fichier ".../yang.py", line 8, in combiner
return number + yin.number
AttributeError: module 'yin' has no attribute 'number'
The error message may seem a bit puzzling at first. Looking back at the source code, you can confirm that number
is defined in the yin
module.
The problem is that number
isn’t defined in yin
at the time yang
gets imported. Consequently, yin.number
is used by the call to combine()
.
To add to the confusion, you’ll have no issues importing yang
:
>>> import yang
Hello from yang
Hello from yin
Goodbye from yin
yin and yang combined is 66
Goodbye from yang
By the time yang
calls combine()
, yin
is fully imported and yin.number
is well defined. As a final twist, because of the module cache you saw earlier, import yin
might work if you do some other imports first:
>>> import yang
Hello from yang
Hello from yin
Goodbye from yin
yin and yang combined is 66
Goodbye from yang
>>> yin
Traceback (most recent call last):
Fichier "" , line 1, in
NameError: name 'yin' is not defined
>>> import yin
>>> yin.combiner()
66
So how can you avoid being bogged down and confused by cyclical imports? Having two or more modules importing each other is often a sign that you can improve the design of your modules.
Often, the easiest time to fix cyclical imports is avant you implement them. If you see cycles in your architecture sketches, have a closer look and try to break the cycles.
Still, there are times when it’s reasonable to introduce an import cycle. As you saw above, this isn’t a problem so long as your modules define only attributes, functions, classes, and so on. The second tip—which is also good design practice—is to keep your modules free of side effects at import time.
If you really need modules with import cycles and side effects, there’s still another way out: do your imports locally inside functions.
Note that in the following code, import yang
is done inside combine()
. This has two consequences. Première, yang
is available only inside the combine()
fonction. More importantly, the import doesn’t happen until you call combine()
après yin
has been fully imported:
# yin.py
impression(F"Hello from yin")
number = 42
def combiner():
import yang
return number + yang.number
impression(F"Goodbye from yin")
# yang.py
impression(F"Hello from yang")
import yin
number = 24
def combiner():
return number + yin.number
impression(F"yin and yang combined is combiner()")
impression(F"Goodbye from yang")
Now there are no issues importing and using yin
:
>>> import yin
Hello from yin
Goodbye from yin
>>> yin.combiner()
Hello from yang
yin and yang combined is 66
Goodbye from yang
66
Notice that yang
is, in fact, not imported until you call combine()
. For another perspective on cyclical imports, see Fredrik Lundh’s classic note.
Profile Imports
One concern when importing several modules and packages is that it will add to the startup time of your script. Depending on your application, this may or may not be critical.
Since the release of Python 3.7, you’ve had a quick way of knowing how much time it takes to import packages and modules. Python 3.7 supports the -X importtime
command-line option, which measures and prints how much time each module takes to import:
$ python -X importtime -c "import datetime"
import time: self [us] | cumulative | imported package
...
import time: 87 | 87 | temps
import time: 180 | 180 | math
import time: 234 | 234 | _datetime
import time: 820 | 1320 | datetime
le cumulative
column shows the cumulative time of import (in microseconds) on a per-package basis. You can read the listing as follows: Python spent 1320
microseconds to fully import datetime
, which involved importing temps
, math
, and the C implementation _datetime
ainsi que.
le self
column shows the time it took to import only the given module, excluding any recursive imports. You can see that temps
took 87
microseconds to import, math
took 180
, _datetime
took 234
, and the import of datetime
itself took 820
microseconds. All in all, this adds up to a cumulative time of 1320
microseconds (within rounding errors).
Have a look at the countdown.py
example from the Colorama section:
$ python3.7 -X importtime countdown.py
import time: self [us] | cumulative | imported package
...
import time: 644 | 7368 | colorama.ansitowin32
import time: 310 | 11969 | colorama.initialise
import time: 333 | 12301 | colorama
import time: 297 | 12598 | optional_color
import time: 119 | 119 | temps
In this example, importing optional_color
took almost 0.013 seconds. Most of that time was spent importing Colorama and its dependencies. le self
column shows the import time excluding nested imports.
For an extreme example, consider the population
singleton from earlier. Because it’s loading a big data file, it’s extremely slow to import. To test this, you can run import population
as a script with the -c
option:
$ python3.7 -X importtime -c "import population"
import time: self [us] | cumulative | imported package
...
import time: 4933 | 322111 | matplotlib.pyplot
import time: 1474 | 1474 | typing
import time: 420 | 1894 | importlib.resources
Reading population data for Medium scenario
import time: 1593774 | 1921024 | population
In this case, it takes almost 2 seconds to import population
, of which about 1.6 seconds are spent in the module itself, mainly for loading the data file.
-X importtime
is a great tool for optimizing your imports. If you need to do more general monitoring and optimization of your code, then check out Python Timer Functions: Three Ways to Monitor Your Code.
Conclusion
In this tutorial, you’ve gotten to know the Python import system. Like many things in Python, it’s fairly straightforward to use for basic tasks like importing modules and packages. At the same time, the import system is quite complex, flexible, and extendable. You’ve learned several import-related tricks that you can take advantage of in your own code.
In this tutorial, you’ve learned how to:
- Créer namespace packages
- Import resources et data files
- Decide what to import dynamically at runtime
- Extend Python’s import system
- Handle different versions of packages
Throughout the tutorial, you’ve seen many links to further info. The most authoritative source on the Python import system is the official documentation:
You can put your knowledge of Python imports to use by following along with the examples in this tutorial. Click the link below for access to the source code:
[ad_2]