Qu'est-ce que le dossier __pycache__ en Python ?
Lorsque vous développez un script Python autonome, vous ne remarquerez peut-être rien d'inhabituel dans la structure de vos répertoires. Cependant, dès que votre projet devient plus complexe, vous déciderez souvent d’extraire des parties des fonctionnalités dans des modules ou packages supplémentaires. C'est à ce moment-là que vous pouvez commencer à voir un dossier __pycache__
apparaître de nulle part à côté de vos fichiers sources à des endroits apparemment aléatoires :
project/
│
├── mathematics/
│ │
│ ├── __pycache__/
│ │
│ ├── arithmetic/
│ │ ├── __init__.py
│ │ ├── add.py
│ │ └── sub.py
│ │
│ ├── geometry/
│ │ │
│ │ ├── __pycache__/
│ │ │
│ │ ├── __init__.py
│ │ └── shapes.py
│ │
│ └── __init__.py
│
└── calculator.py
Notez que le dossier __pycache__
peut être présent à différents niveaux dans l'arborescence de répertoires de votre projet lorsque vous avez plusieurs sous-packages imbriqués les uns dans les autres. Dans le même temps, d'autres packages ou dossiers contenant vos fichiers sources Python peuvent ne pas contenir ce mystérieux répertoire de cache.
Remarque : Pour conserver un espace de travail plus propre, de nombreux IDE et éditeurs de code Python sont configurés immédiatement pour vous masquer les dossiers __pycache__
, même si ces dossiers existent. sur votre système de fichiers.
Vous pouvez rencontrer une situation similaire après avoir cloné un référentiel Git distant avec un projet Python et exécuté le code sous-jacent. Alors, qu'est-ce qui provoque l'apparition du dossier __pycache__
et dans quel but ?
En bref : cela accélère l’importation de modules Python
Même si Python est un langage de programmation interprété, son interpréteur n’opère pas directement sur votre code Python, ce qui serait très lent. Au lieu de cela, lorsque vous exécutez un script Python ou importez un module Python, l'interpréteur compile votre code source Python de haut niveau en bytecode, qui est une représentation binaire intermédiaire du code.
Ce bytecode permet à l'interpréteur d'ignorer les étapes récurrentes, telles que le lexage et l'analyse du code dans un arbre de syntaxe abstraite et de valider son exactitude à chaque fois que vous exécutez le même programme. Tant que le code source sous-jacent n’a pas changé, Python peut réutiliser la représentation intermédiaire, qui est immédiatement prête à être exécutée. Cela permet de gagner du temps et d'accélérer le temps de démarrage de votre script.
N'oubliez pas que même si le chargement du bytecode compilé depuis __pycache__
accélère l'importation des modules Python, cela n'affecte pas leur vitesse d'exécution !
Pourquoi s'embêter avec le bytecode au lieu de compiler le code directement dans le code machine de bas niveau ? Bien que le code machine soit ce qui s'exécute sur le matériel, offrant ainsi les performances ultimes, il n'est pas aussi portable ni aussi rapide à produire que le bytecode.
Le code machine est un ensemble d'instructions binaires comprises par votre architecture CPU spécifique, enveloppées dans un format de conteneur tel que EXE, ELF ou Mach-O, selon le système d'exploitation. En revanche, le bytecode fournit une couche d’abstraction indépendante de la plate-forme et est généralement plus rapide à compiler.
Python utilise les dossiers __pycache__
locaux pour stocker le bytecode compilé des modules importés dans votre projet. Lors des exécutions suivantes, l'interpréteur tentera de charger des versions précompilées des modules à partir de ces dossiers, à condition qu'ils soient à jour avec les fichiers sources correspondants. Notez que ce mécanisme de mise en cache n'est déclenché que pour les modules que vous importez dans votre code plutôt que de les exécuter en tant que scripts dans le terminal.
En plus de cette mise en cache du bytecode sur disque, Python conserve un cache en mémoire des modules, auquel vous pouvez accéder via sys.modules
dictionnaire. Cela garantit que lorsque vous importez le même module plusieurs fois à partir de différents endroits de votre programme, Python utilisera le module déjà importé sans avoir besoin de le recharger ou de le recompiler. Les deux mécanismes fonctionnent ensemble pour réduire la surcharge liée à l'importation de modules Python.
Ensuite, vous découvrirez exactement à quelle vitesse Python charge le bytecode mis en cache plutôt que de compiler le code source à la volée lorsque vous importez un module.
À quel point le chargement des modules à partir du cache est-il plus rapide ?
La mise en cache s'effectue en coulisse et passe généralement inaperçue car Python est assez rapide pour compiler le bytecode. De plus, à moins d’exécuter souvent des scripts Python éphémères, l’étape de compilation reste insignifiante par rapport au temps total d’exécution. Cela dit, sans mise en cache, la surcharge associée à la compilation du bytecode pourrait s'accumuler si vous aviez beaucoup de modules et que vous les importiez plusieurs fois.
Pour mesurer la différence de temps d'importation entre un module mis en cache et un module non mis en cache, vous pouvez transmettre l'option -X importtime
à la commande python
ou définir le variable d'environnement PYTHONPROFILEIMPORTTIME
équivalente. Lorsque cette option est activée, Python affichera un tableau résumant le temps nécessaire à l'importation de chaque module, y compris le temps cumulé dans le cas où un module dépend d'autres modules.
Supposons que vous disposiez d'un script calculator.py
qui importe et appelle une fonction utilitaire à partir d'un module arithmetic.py
local :
from arithmetic import add
add(3, 4)
Le module importé définit une seule fonction :
def add(a, b):
return a + b
Comme vous pouvez le voir, le script principal délègue l'ajout de deux nombres, trois et quatre, à la fonction add()
importée du module arithmétique
.
Remarque : Même si vous utilisez la syntaxe from ... import
, qui amène uniquement le symbole spécifié dans votre espace de noms actuel, Python lit et compile quand même l'intégralité du module. De plus, les importations non utilisées déclencheraient également la compilation.
La première fois que vous exécutez votre script, Python compile et enregistre le bytecode du module que vous avez importé dans un dossier local __pycache__
. Si un tel dossier n’existe pas déjà, Python en crée un automatiquement avant de continuer. Désormais, lorsque vous exécutez à nouveau votre script, Python devrait trouver et charger le bytecode mis en cache tant que vous n'avez pas modifié le code source associé.
Une fois le cache réchauffé, il contribuera à un temps de démarrage plus rapide de votre script Python :
$ python -X importtime calculator.py
(...)
import time: 20092 | 20092 | arithmetic
$ python -X importtime calculator.py
(...)
import time: 232 | 232 | arithmetic
$ python -X importtime calculator.py
(...)
import time: 203 | 203 | arithmetic
Bien que les mesures exactes puissent varier d’une série à l’autre, l’amélioration est clairement visible. Sans le dossier __pycache__
, la tentative initiale d'importation de arithmétique
était deux ordres de grandeur plus lente que lors des exécutions suivantes. Ce changement peut paraître stupéfiant à première vue, mais ces valeurs sont exprimées en microsecondes, vous ne remarquerez donc probablement même pas la différence malgré une baisse aussi spectaculaire des chiffres.
Le gain de performances est généralement à peine reflété par le temps de démarrage de la plupart des scripts Python, que vous pouvez évaluer à l'aide de la commande time
sur les systèmes de type Unix :
$ rm -rf __pycache__
$ time python calculator.py
real 0m0.088s
user 0m0.064s
sys 0m0.028s
$ time python calculator.py
real 0m0.086s
user 0m0.060s
sys 0m0.030s
Ici, le temps d'exécution total reste pratiquement le même, que le cache existe ou non. La suppression du dossier __pycache__
retarde l'exécution d'environ deux millisecondes, ce qui est négligeable pour la plupart des applications.
Le compilateur de bytecode de Python est assez rapide lorsque vous le comparez à un homologue plus sophistiqué de Java, qui peut tirer parti du typage statique.
Par exemple, si vous disposez d'un exemple de fichier Calculator.java
, vous pouvez le compiler à l'avance dans un fichier .class
, ce qui est la manière habituelle de travailler avec du code Java. , ou exécutez directement le fichier .java
. Dans ce dernier cas, le runtime Java compilera le code en arrière-plan dans un emplacement temporaire avant de l'exécuter :
$ javac Calculator.java
$ time java Calculator
real 0m0.039s
user 0m0.026s
sys 0m0.019s
$ time java Calculator.java
real 0m0.574s
user 0m1.182s
sys 0m0.069s
Lorsque vous compilez manuellement le code source à l'aide de la commande javac
et exécutez le bytecode résultant, l'exécution prend environ quarante millisecondes. En revanche, lorsque vous laissez la commande java
gérer la compilation, le temps total d'exécution s'élève à un peu plus d'une demi-seconde. Par conséquent, contrairement à Python, la surcharge du compilateur Java est très perceptible, même avec une compilation parallèle sur plusieurs cœurs CPU !
Maintenant que vous connaissez le but du dossier __pycache__
, vous pourriez être curieux de connaître son contenu.
Que contient un dossier __pycache__
?
Lorsque vous jetez un coup d'œil dans le dossier __pycache__
, vous verrez un ou plusieurs fichiers se terminant par l'extension .pyc
. Il représente un module Python compilé :
$ ls -1 __pycache__
arithmetic.cpython-311.pyc
arithmetic.cpython-312.pyc
arithmetic.pypy310.opt-1.pyc
arithmetic.pypy310.opt-2.pyc
arithmetic.pypy310.pyc
solver.cpython-312.pyc
units.cpython-312.pyc
Chacun de ces fichiers contient le bytecode du module Python correspondant, défini dans le package actuel, que vous avez importé lors de l'exécution. Le bytecode compilé cible une implémentation, une version et un niveau d'optimisation facultatif de Python spécifiques. Toutes ces informations sont codées dans le nom du fichier.
Par exemple, un fichier nommé arithmetic.pypy310.opt-2.pyc
est le bytecode de votre module arithmetic.py
compilé par PyPy 3.10 avec un niveau d'optimisation de deux. Cette optimisation supprime les instructions assert
et supprime toutes les docstrings. A l'inverse, arithmetic.cpython-312.pyc
représente le même module mais compilé pour CPython 3.12 sans aucune optimisation.
Au total, il existe cinq variantes de bytecode compilées à partir d'un seul module arithmetic.py
dans le dossier __pycache__
ci-dessus, qui sont mises en évidence.
Remarque : Avant Python 3.5, l'activation de l'un des niveaux d'optimisation avec l'indicateur -O
ou -OO
compilerait le bytecode dans un .pyo
dans le dossier cache. Cela a changé après l'entrée en vigueur de la PEP 488, et le niveau d'optimisation facultatif est désormais codé dans le nom du fichier .pyc
.
Un tel schéma de dénomination de fichiers garantit la compatibilité entre les différentes versions et saveurs de Python. Lorsque vous exécutez le même script à l'aide de PyPy ou d'une version antérieure de CPython, l'interpréteur compilera tous les modules importés dans son propre environnement d'exécution afin de pouvoir les réutiliser ultérieurement. La version Python, ainsi que d'autres métadonnées, sont également stockées dans le fichier .pyc
lui-même.
Vous examinerez de plus près les fichiers .pyc
stockés dans le dossier cache à la fin de ce didacticiel. Il est maintenant temps de découvrir les circonstances qui poussent Python à créer le dossier de cache.
Quand Python crée-t-il des dossiers de cache ?
L'interpréteur stockera uniquement le bytecode compilé des modules Python dans le dossier __pycache__
lorsque vous importerez ces modules ou parfois leur package parent. Il ne créera pas le dossier de cache lorsque vous exécutez un script Python standard qui n'importe aucun module ou package. Ceci est basé sur l'hypothèse que les modules sont moins susceptibles de changer et que vous pouvez les importer plusieurs fois au cours d'une seule exécution.
Remarque : Bien que vous ne soyez exposé à la compilation de bytecode que via le dossier __pycache__
lorsque vous importez des modules ou des packages locaux, la machinerie sous-jacente fonctionne également bien pour packages de bibliothèques tierces et standard.
Lorsque vous installez un package tiers dans un environnement virtuel, par défaut, pip
précompilera les modules du package dans des fichiers .pyc
à l'aide de l'interpréteur Python qu'il exécute actuellement. sur. De même, les modules purement Python de la bibliothèque standard de Python sont précompilés avec tous les niveaux d’optimisation possibles.
Si vous deviez supprimer l'un de ces fichiers .pyc
, soit du dossier site-packages/
de l'environnement virtuel, soit du dossier lib/
de Python, alors l'interpréteur le recréera la prochaine fois que vous importerez le module correspondant.
Lorsque vous importez un module individuel à partir d'un package, Python produira le fichier .pyc
correspondant et le mettra en cache dans le dossier __pycache__
situé à l'intérieur de ce package. . Il compilera également le fichier __init__.py
du package, mais ne touchera aucun autre module ou sous-paquet imbriqué. Cependant, si le module importé importe lui-même d’autres modules, alors ces modules seront également compilés, et ainsi de suite.
Voici un exemple illustrant le cas le plus élémentaire, en supposant que vous ayez placé une instruction import
appropriée dans le script calculator.py
ci-dessous :
project/
│
├── arithmetic/
│ │
│ ├── __pycache__/
│ │ ├── __init__.cpython-312.pyc
│ │ └── add.cpython-312.pyc
│ │
│ ├── __init__.py
│ ├── add.py
│ └── sub.py
│
└── calculator.py
Après avoir exécuté le script pour la première fois, Python s'assurera qu'un dossier __pycache__
existe dans le package arithmetic
. Ensuite, il compilera le __init__.py
du package ainsi que tous les modules importés de ce package. Dans ce cas, vous avez uniquement demandé d'importer le module arithmetic.add
, afin que vous puissiez voir un fichier .pyc
associé à add.py
mais pas avec sub.py
.
Toutes les instructions import
suivantes donneraient le même résultat décrit ci-dessus :
import arithmetic.add
from arithmetic import add
from arithmetic.add import add_function
Quelle que soit la manière dont vous importez un module Python et que vous l'importiez dans son intégralité ou simplement un symbole spécifique, tel qu'une classe ou une constante, l'interpréteur compile l'intégralité du module, car il ne peut pas lire les modules partiellement.
À l'inverse, l'importation d'un package entier amènerait normalement Python à compiler uniquement __init__.py
dans ce package. Cependant, il est assez courant que les packages exposent leurs modules ou sous-packages internes depuis __init__.py
pour un accès plus pratique. Par exemple, considérez ceci :
from arithmetic import add
from arithmetic.sub import sub_function
Les importations dans __init__.py
comme celles-ci créent des fichiers .pyc
supplémentaires, même lorsque vous utilisez l'instruction simple import arithmétique
dans votre script.
Si vous avez importé un sous-paquet ou un module ou un symbole profondément imbriqué, alors tous les packages intermédiaires menant au package de niveau supérieur verraient également leurs fichiers __init__.py
compilés et placés. dans leurs dossiers de cache respectifs. Cependant, Python n’ira pas dans l’autre sens en analysant de manière récursive les sous-paquets imbriqués, car cela serait inutile. Il compilera uniquement les modules dont vous avez réellement besoin en les important explicitement ou indirectement.
Remarque : Le simple fait d'importer un package ou un module déclenche la compilation. Vous pouvez avoir des instructions d'importation inutilisées dans votre code, et Python les compilera quand même !
Que se passe-t-il si Python a déjà compilé votre module dans un fichier .pyc
, mais que vous décidez de modifier son code source dans le fichier .py
d'origine ? Vous le saurez ensuite !
Quelles actions invalident le cache ?
L’exécution d’un bytecode obsolète pourrait provoquer une erreur ou, pire encore, conduire à un comportement complètement imprévisible. Heureusement, Python est suffisamment intelligent pour détecter lorsque vous modifiez le code source d'un module compilé et le recompilera si nécessaire.
Pour déterminer si un module doit être recompilé, Python utilise l'une des deux stratégies d'invalidation du cache :
- Basé sur l'horodatage
- Basé sur le hachage
La première compare la taille du fichier source et son horodatage de dernière modification aux métadonnées stockées dans le fichier .pyc
correspondant. Plus tard, vous découvrirez comment ces valeurs sont conservées dans le fichier .pyc
, avec d'autres métadonnées.
En revanche, la deuxième stratégie calcule la valeur de hachage du fichier source et la compare à un champ spécial dans l'en-tête du fichier (PEP 552), introduit dans Python 3.7. Cette stratégie est plus sécurisée et déterministe mais aussi un peu plus lente. C’est pourquoi la stratégie basée sur l’horodatage reste pour l’instant la stratégie par défaut.
Lorsque vous mettez à jour artificiellement l'heure de modification (mtime
) du fichier source, par exemple en utilisant la commande touch
sur macOS ou Linux, vous forcez Python à compiler le fichier source. module à nouveau :
$ tree -D --dirsfirst
[Apr 26 09:48] .
├── [Apr 26 09:48] __pycache__
│ └── [Apr 26 09:48] arithmetic.cpython-312.pyc
├── [Apr 26 09:48] arithmetic.py
└── [Apr 26 09:48] calculator.py
2 directories, 3 files
$ touch arithmetic.py
$ python calculator.py
$ tree -D --dirsfirst
[Apr 26 09:48] .
├── [Apr 26 09:52] __pycache__
│ └── [Apr 26 09:52] arithmetic.cpython-312.pyc
├── [Apr 26 09:52] arithmetic.py
└── [Apr 26 09:48] calculator.py
2 directories, 3 files
Initialement, le fichier arithmetic.cpython-312.pyc
mis en cache a été modifié pour la dernière fois à 09h48. Après avoir touché le fichier source, arithmetic.py
, Python considère le bytecode compilé comme obsolète et recompile le module lorsque vous exécutez le script qui importe arithmetic
. Cela génère un nouveau fichier .pyc
avec un horodatage mis à jour à 09h52.
Pour produire des fichiers .pyc
basés sur le hachage, vous devez utiliser le module compileall
de Python avec l'option --invalidation-mode
définie en conséquence. Par exemple, cette commande compilera tous les modules du dossier et des sous-dossiers actuels dans la variante basée sur le hachage dite vérifiée :
$ python -m compileall --invalidation-mode checked-hash
La documentation officielle explique la différence entre les variantes cochées et non cochées des fichiers .pyc
basés sur le hachage comme suit :
Pour les fichiers
.pyc
basés sur le hachage, Python valide le fichier cache en hachant le fichier source et en comparant le hachage obtenu avec le hachage du fichier cache. Si un fichier cache basé sur le hachage vérifié s'avère invalide, Python le régénère et écrit un nouveau fichier cache basé sur le hachage vérifié. Pour les fichiers.pyc
basés sur un hachage non vérifiés, Python suppose simplement que le fichier cache est valide s'il existe. (Source)
Néanmoins, vous pouvez toujours remplacer le comportement de validation par défaut des fichiers .pyc
basés sur le hachage avec l'option --check-hash-based-pycs
lorsque vous exécutez l'interpréteur Python.
Savoir quand et où Python crée les dossiers __pycache__
, ainsi que quand il met à jour leur contenu, vous donnera une idée de la sécurité de leur suppression.
Est-il sécuritaire de supprimer un dossier de cache ?
Oui, même si vous devriez vous demander si vous devriez vraiment le faire ! À ce stade, vous comprenez que supprimer un dossier __pycache__
est inoffensif car Python régénère le cache à chaque invocation. Quoi qu'il en soit, supprimer manuellement les dossiers de cache individuels est un travail fastidieux. De plus, cela ne durera que jusqu’à la prochaine exécution de votre code.
Remarque : Bien que vous n'ayez pas besoin de supprimer les dossiers de cache dans la plupart des cas, il est recommandé de les exclure d'un système de contrôle de version comme Git, par exemple, en ajoutant un modèle de fichier approprié au fichier de votre projet. .gitignore
. De plus, si votre éditeur de code ne l'a pas déjà fait, vous souhaiterez peut-être le configurer pour masquer ces dossiers de l'explorateur de fichiers afin d'éviter toute distraction.
La bonne nouvelle est que vous pouvez automatiser la suppression de ces dossiers de cache de votre projet si vous insistez vraiment, ce que vous allez faire maintenant.
Comment supprimer de manière récursive tous les dossiers de cache ?
D'accord, vous avez déjà établi que supprimer le bytecode mis en cache compilé par Python n'est pas un gros problème. Cependant, le problème avec ces dossiers __pycache__
est qu'ils peuvent apparaître dans plusieurs sous-répertoires si vous avez une structure de projet complexe. Les trouver et les supprimer à la main serait une corvée, d'autant plus qu'ils renaissent comme un phénix de leurs cendres à chaque fois que vous exécutez Python.
Ci-dessous, vous trouverez les commandes spécifiques à la plate-forme qui suppriment de manière récursive tous les dossiers __pycache__
du répertoire actuel et de tous ses sous-répertoires imbriqués en une seule fois :
Le premier extrait de code doit être utilisé sous Windows et le deuxième extrait de code est pour Linux + macOS :
PS> $dirs = Get-ChildItem -Path . -Filter __pycache__ -Recurse -Directory
PS> $dirs | Remove-Item -Recurse -Force
$ find . -type d -name __pycache__ -exec rm -rf {} +
Vous devez faire preuve de prudence lorsque vous exécutez des commandes de suppression groupée, car elles peuvent supprimer plus que prévu si vous ne faites pas attention. Vérifiez toujours les chemins et les critères de filtrage avant d’exécuter de telles commandes !
La suppression des dossiers __pycache__
peut désencombrer l'espace de travail de votre projet, mais seulement provisoirement. Si vous êtes toujours ennuyé de devoir exécuter à plusieurs reprises la commande de suppression récursive, vous préférerez peut-être prendre le contrôle de la gestion du dossier de cache en premier lieu. Ensuite, vous explorerez deux approches pour résoudre ce problème.
Comment empêcher Python de créer des dossiers de cache ?
Si vous ne souhaitez pas que Python mette en cache le bytecode compilé, vous pouvez transmettre l'option -B
à la commande python
lors de l'exécution d'un script. Cela empêchera les dossiers __pycache__
d'apparaître à moins qu'ils n'existent déjà. Cela dit, Python continuera à exploiter tous les fichiers .pyc
qu'il peut trouver dans les dossiers de cache existants. Il n’écrira tout simplement pas de nouveaux fichiers sur le disque.
Pour un effet plus permanent et global qui s'étend sur plusieurs interpréteurs Python, vous pouvez définir la variable d'environnement PYTHONDONTWRITEBYTECODE
dans votre shell ou son fichier de configuration :
export PYTHONDONTWRITEBYTECODE=1
Cela affectera n’importe quel interpréteur Python, y compris celui d’un environnement virtuel que vous avez activé.
Néanmoins, vous devez réfléchir attentivement pour savoir si la suppression de la compilation de bytecode est la bonne approche pour votre cas d'utilisation. L'alternative consiste à demander à Python de créer les dossiers __pycache__
individuels dans un seul emplacement partagé sur votre système de fichiers.
Comment stocker le cache dans un dossier centralisé ?
Lorsque vous désactivez complètement la compilation du bytecode, vous obtenez un espace de travail plus propre mais perdez les avantages de la mise en cache pour des temps de chargement plus rapides. Si vous souhaitez combiner le meilleur des deux mondes, vous pouvez demander à Python d'écrire les fichiers .pyc
dans une arborescence parallèle enracinée dans le répertoire spécifié à l'aide du -X pycache_prefix
option :
$ python -X pycache_prefix=/tmp/pycache calculator.py
Dans ce cas, vous demandez à Python de mettre en cache le bytecode compilé dans un dossier temporaire situé dans /tmp/pycache
sur votre système de fichiers. Lorsque vous exécutez cette commande, Python n'essaiera plus de créer des dossiers __pycache__
locaux dans votre projet. Au lieu de cela, il reflétera la structure de répertoires de votre projet sous le dossier racine indiqué et y stockera tous les fichiers .pyc
:
tmp/
└── pycache/
└── home/
└── user/
│
├── other_project/
│ └── solver.cpython-312.pyc
│
└── project/
│
└── mathematics/
│
├── arithmetic/
│ ├── __init__.cpython-312.pyc
│ ├── add.cpython-312.pyc
│ └── sub.cpython-312.pyc
│
├── geometry/
│ ├── __init__.cpython-312.pyc
│ └── shapes.cpython-312.pyc
│
└── __init__.cpython-312.pyc
Remarquez deux choses ici. Premièrement, comme le répertoire de cache est séparé de votre code source, il n'est pas nécessaire d'imbriquer les fichiers .pyc
compilés dans les dossiers __pycache__
. Deuxièmement, puisque la hiérarchie au sein d’un tel cache centralisé correspond à la structure de votre projet, vous pouvez partager ce dossier de cache entre plusieurs projets.
D'autres avantages de cette configuration incluent un nettoyage plus facile, car vous pouvez supprimer tous les fichiers .pyc
appartenant au même projet avec une seule frappe sans avoir à parcourir manuellement tous les répertoires. De plus, vous pouvez stocker le dossier de cache sur un disque physique distinct pour profiter des lectures parallèles, ou conserver le cache dans un volume persistant lorsque vous travaillez avec des conteneurs Docker.
N'oubliez pas que vous devez utiliser l'option -X pycache_prefix
chaque fois que vous exécutez la commande python
pour que cela fonctionne de manière cohérente. Comme alternative, vous pouvez définir le chemin d'accès à un dossier de cache partagé via la variable d'environnement PYTHONPYCACHEPREFIX
:
export PYTHONPYCACHEPREFIX=/tmp/pycache
Dans tous les cas, vous pouvez vérifier par programme si Python utilisera le répertoire de cache spécifié ou reviendra au comportement par défaut et créera des dossiers __pycache__
locaux :
>>> import sys
>>> sys.pycache_prefix
'/tmp/pycache'
La variable sys.pycache_prefix
peut être une chaîne ou Aucun
.
Vous avez parcouru un long chemin grâce à ce didacticiel et vous savez maintenant une ou deux choses sur la gestion des dossiers __pycache__
dans vos projets Python. Il est enfin temps de voir comment vous pouvez travailler directement avec les fichiers .pyc
stockés dans ces dossiers.
Que contient un fichier .pyc
mis en cache ?
Un fichier .pyc
se compose d'un en-tête avec des métadonnées suivi de l'objet de code sérialisé à exécuter au moment de l'exécution. L'en-tête du fichier commence par un nombre magique qui identifie de manière unique la version spécifique de Python pour laquelle le bytecode a été compilé. Ensuite, il y a un champ de bits défini dans le PEP 552, qui détermine l'une des trois stratégies d'invalidation du cache expliquées précédemment.
Dans les fichiers basés sur l'horodatage .pyc
, le champ de bits est rempli de zéros et suivi de deux champs de quatre octets. Ces champs correspondent respectivement à l'heure Unix de la dernière modification et à la taille du fichier source .py
:
Offset | Field Size | Field | Description |
---|---|---|---|
0 | 4 | Magic number | Identifies the Python version |
4 | 4 | Bit field | Filled with zeros |
8 | 4 | Timestamp | The time of .py file’s modification |
12 | 4 | File size | Concerns the source .py file |
À l'inverse, pour les fichiers basés sur un hachage .pyc
, le champ de bits peut être égal à un, indiquant une variante non cochée, ou à trois, ce qui signifie la variante cochée. Ensuite, au lieu de l'horodatage et de la taille du fichier, il n'y a qu'un seul champ de huit octets avec la valeur de hachage du code source Python :
Offset | Field Size | Field | Description |
---|---|---|---|
0 | 4 | Magic number | Identifies the Python version |
4 | 4 | Bit field | Equals 1 (unchecked) or 3 (checked) |
8 | 8 | Hash value | Source code’s hash value |
Dans les deux cas, l’en-tête fait seize octets, que vous pouvez ignorer si vous ne souhaitez pas lire les métadonnées codées. Ce faisant, vous accéderez directement à l'objet de code sérialisé avec le module marshal
, qui occupe la partie restante du fichier .pyc
.
Avec ces informations, vous pouvez radiographier l'un de vos fichiers .pyc
compilés et exécuter directement le bytecode sous-jacent, même si vous n'avez plus le fichier .py
d'origine avec le code source associé.
Comment lire et exécuter le bytecode mis en cache ?
En lui-même, Python n'exécutera un fichier compagnon .pyc
que si le fichier .py
d'origine existe toujours. Si vous supprimez le module source après qu'il ait déjà été compilé, Python refusera d'exécuter le fichier .pyc
. C'est par conception. Cependant, vous pouvez toujours exécuter le bytecode manuellement si vous le souhaitez.
Remarque : Si vous lisez ceci dans un avenir lointain, il est possible que le format de fichier .pyc
sous-jacent ou l'API de la bibliothèque standard ait changé. Ainsi, le script que vous êtes sur le point de voir nécessitera peut-être quelques ajustements avant de fonctionner comme prévu avec la version actuelle de Python. Alternativement, vous pouvez l'exécuter avec Python 3.12 pour garantir la compatibilité.
Le script Python suivant montre comment lire l'en-tête du fichier .pyc
, ainsi que comment désérialiser et exécuter le bytecode qui l'accompagne :
import marshal
from datetime import datetime, timezone
from importlib.util import MAGIC_NUMBER
from pathlib import Path
from pprint import pp
from py_compile import PycInvalidationMode
from sys import argv
from types import SimpleNamespace
def main(path):
metadata, code = load_pyc(path)
pp(vars(metadata))
if metadata.magic_number == MAGIC_NUMBER:
exec(code, globals())
else:
print("Bytecode incompatible with this interpreter")
def load_pyc(path):
with Path(path).open(mode="rb") as file:
return (
parse_header(file.read(16)),
marshal.loads(file.read()),
)
def parse_header(header):
metadata = SimpleNamespace()
metadata.magic_number = header[0:4]
metadata.magic_int = int.from_bytes(header[0:4][:2], "little")
metadata.python_version = f"3.{(metadata.magic_int - 2900) // 50}"
metadata.bit_field = int.from_bytes(header[4:8], "little")
metadata.pyc_type = {
0: PycInvalidationMode.TIMESTAMP,
1: PycInvalidationMode.UNCHECKED_HASH,
3: PycInvalidationMode.CHECKED_HASH,
}.get(metadata.bit_field)
if metadata.pyc_type is PycInvalidationMode.TIMESTAMP:
metadata.timestamp = datetime.fromtimestamp(
int.from_bytes(header[8:12], "little"),
timezone.utc,
)
metadata.file_size = int.from_bytes(header[12:16], "little")
else:
metadata.hash_value = header[8:16]
return metadata
if __name__ == "__main__":
main(argv[1])
Il se passe plusieurs choses ici, vous pouvez donc les décomposer ligne par ligne :
- La Ligne 11 reçoit un tuple comprenant les métadonnées analysées à partir de l'en-tête du fichier et un objet de code désérialisé prêt à être exécuté. Les deux sont chargés à partir d'un fichier
.pyc
spécifié comme seul argument de ligne de commande requis. - La Ligne 12 imprime joliment les métadonnées décodées sur l'écran.
- Les lignes 13 à 16 exécutent conditionnellement le bytecode du fichier
.pyc
en utilisantexec()
ou impriment un message d'erreur. Pour déterminer si le fichier a été compilé pour la version actuelle de l’interpréteur, ce fragment de code compare le nombre magique obtenu à partir de l’en-tête au nombre magique de l’interpréteur. Si tout réussit, les symboles du module chargé sont importés dansglobals()
. - Les lignes 18 à 23 ouvrent le fichier
.pyc
en mode binaire à l'aide du modulepathlib
, analysent l'en-tête et désorganisent l'objet code. - Les lignes 25 à 44 analysent les champs d'en-tête en utilisant leurs décalages et tailles d'octets correspondants, en interprétant les valeurs multi-octets avec l'ordre des octets petit-boutiste.
- Les lignes 28 et 29 extraient la version Python du nombre magique, qui s'incrémente à chaque version mineure de Python selon la formule 2900 + 50n, où n est la version mineure de Python 3.11 ou ultérieure.
- Les lignes 31 à 35 déterminent le type de fichier
.pyc
en fonction du champ de bits précédent (PEP 552). - Les lignes 37 à 40 convertissent l'heure de modification du fichier source en un objet
datetime
dans le fuseau horaire UTC.
Vous pouvez exécuter le script X-ray décrit ci-dessus sur un fichier .pyc
de votre choix. Lorsque vous activez le mode interactif de Python (-i
), vous pourrez inspecter les variables et l'état du programme une fois celui-ci terminé :
$ python -i xray.py __pycache__/arithmetic.cpython-312.pyc
{'magic_number': b'\xcb\r\r\n',
'magic_int': 3531,
'python_version': '3.12',
'bit_field': 0,
'pyc_type': <PycInvalidationMode.TIMESTAMP: 1>,
'timestamp': datetime.datetime(2024, 4, 26, 17, 34, 57, tzinfo=….utc),
'file_size': 32}
>>> add(3, 4)
7
Le script imprime les champs d'en-tête décodés, y compris le nombre magique et l'heure de modification du fichier source. Juste après cela, grâce à l'option python -i
, vous êtes déposé dans le REPL Python interactif où vous appelez add()
, qui a été importé dans l'espace de noms global par exécuter le bytecode du module.
Cela fonctionne comme prévu car l'interpréteur Python que vous exécutez actuellement correspond à la version du bytecode du module. Voici ce qui se passerait si vous essayiez d'exécuter un autre fichier .pyc
ciblant une version différente de Python ou l'une de ses implémentations alternatives :
$ python -i xray.py __pycache__/arithmetic.cpython-311.pyc
{'magic_number': b'\xa7\r\r\n',
'magic_int': 3495,
'python_version': '3.11',
'bit_field': 0,
'pyc_type': <PycInvalidationMode.TIMESTAMP: 1>,
'timestamp': datetime.datetime(2024, 4, 25, 14, 40, 26, tzinfo=….utc),
'file_size': 32}
Bytecode incompatible with this interpreter
>>> add(3, 4)
Traceback (most recent call last):
...
NameError: name 'add' is not defined
Cette fois, la sortie affiche un message indiquant une incompatibilité entre la version du bytecode dans le fichier .pyc
et l'interpréteur utilisé. En conséquence, le bytecode n’a pas été exécuté et la fonction add()
n’a pas été définie, vous ne pouvez donc pas l’appeler.
Maintenant, si vous radiographiez un fichier .pyc
basé sur un hachage (coché ou non), voici ce que vous pourriez obtenir :
$ python xray.py __pycache__/arithmetic.cpython-312.pyc
{'magic_number': b'\xcb\r\r\n',
'magic_int': 3531,
'python_version': '3.12',
'bit_field': 3,
'pyc_type': <PycInvalidationMode.CHECKED_HASH: 2>,
'hash_value': b'\xf3\xdd\x87j\x8d>\x0e)'}
Python peut comparer la valeur de hachage incorporée dans le fichier .pyc
avec celle qu'il calcule à partir du fichier .py
associé en appelant source_hash()
sur le code source :
>>> from importlib.util import source_hash
>>> from pathlib import Path
>>> source_hash(Path("arithmetic.py").read_bytes())
b'\xf3\xdd\x87j\x8d>\x0e)'
Il s'agit d'une méthode plus fiable d'invalidation du cache et de vérification de l'intégrité du code que la comparaison d'un attribut volatile de dernière modification du fichier source. Remarquez comment la valeur de hachage calculée concorde avec celle lue dans le fichier .pyc
.
Maintenant que vous savez comment importer des modules Python à partir d'une forme binaire compilée, il peut être tentant de distribuer vos programmes Python commerciaux sans partager le code source.
Le bytecode peut-il masquer les programmes Python ?
Plus tôt, vous avez appris que Python n'importera pas un module à partir d'un fichier .pyc
si le fichier .py
associé est introuvable. Cependant, il existe une exception notable puisque c'est exactement ce qui se produit lorsque vous importez du code Python à partir d'un fichier ZIP spécifié dans le PYTHONPATH
. De telles archives contiennent généralement uniquement les fichiers .pyc
compilés sans le code source qui les accompagne.
La possibilité d'importer des modules compilés, soit manuellement, soit via ces fichiers ZIP, vous permet d'implémenter un mécanisme rudimentaire d'obscurcissement du code. Malheureusement, cela ne serait pas particulièrement à l'épreuve des balles puisque des utilisateurs plus avertis pourraient essayer de décompiler vos fichiers .pyc
en code Python de haut niveau à l'aide d'outils spécialisés comme uncompyle6
ou pycdc
.
Mais même sans ces outils externes, vous pouvez désassembler le bytecode de Python en opcodes lisibles par l’homme, rendant ainsi l’analyse et la rétro-ingénierie de vos programmes assez accessibles. La bonne façon de dissimuler le code source de Python est de le compiler en code machine. Par exemple, vous pouvez vous aider avec des outils comme Cython ou réécrire les parties essentielles de votre code dans un langage de programmation de niveau inférieur comme C, C++ ou Rust.
Comment démonter le bytecode mis en cache ?
Une fois que vous maîtrisez un objet de code en Python, vous pouvez utiliser le module dis
de la bibliothèque standard pour désassembler le bytecode compilé. À titre d'exemple, vous générerez rapidement vous-même un objet de code sans avoir à vous fier aux fichiers .pyc
mis en cache par Python :
>>> from pathlib import Path
>>> source_code = Path("arithmetic.py").read_text(encoding="utf-8")
>>> module = compile(source_code, "arithmetic.py", mode="exec")
>>> module
<code object <module> at 0x7d09a9c92f50, file "arithmetic.py", line 1>
Vous appelez la fonction intégrée compile()
avec le mode "exec"
comme paramètre pour compiler un module Python. Maintenant, vous pouvez afficher les noms d'opcode lisibles par l'homme de l'objet code résultant en utilisant dis
:
>>> from dis import dis
>>> dis(module)
0 0 RESUME 0
1 2 LOAD_CONST 0 (<code object add at 0x7d...>)
4 MAKE_FUNCTION 0
6 STORE_NAME 0 (add)
8 RETURN_CONST 1 (None)
Disassembly of <code object add at 0x7d...>:
1 0 RESUME 0
2 2 LOAD_FAST 0 (a)
4 LOAD_FAST 1 (b)
6 BINARY_OP 0 (+)
10 RETURN_VALUE
Les opcodes MAKE_FUNCTION
et STORE_NAME
vous indiquent qu'il y a une fonction nommée add()
dans ce bytecode. Lorsque vous regardez attentivement l'objet de code désassemblé de cette fonction, vous verrez qu'il prend deux arguments appelés a
et b
, et les ajoute à l'aide de l'opérateur binaire plus (+
), et renvoie la valeur calculée.
Alternativement, vous pouvez parcourir l'arborescence des opcodes et parcourir les instructions individuelles pour essayer de reconstruire le code source Python de haut niveau :
>>> from dis import Bytecode
>>> from types import CodeType
>>> def traverse(code):
... print(code.co_name, code.co_varnames)
... for instruction in Bytecode(code):
... if isinstance(instruction.argval, CodeType):
... traverse(instruction.argval)
...
>>> traverse(module)
<module> ()
add ('a', 'b')
Cet extrait de code est loin d’être complet. De plus, la décompilation est un processus compliqué en général. Cela conduit souvent à des résultats imparfaits, car certaines informations sont perdues de manière irréversible lorsque certaines optimisations sont appliquées lors de la compilation du bytecode. Quoi qu'il en soit, cela devrait vous donner une idée générale du fonctionnement des décompilateurs.
Conclusion
Dans ce didacticiel, vous avez plongé dans le fonctionnement interne du mécanisme de mise en cache du bytecode de Python. Vous comprenez maintenant que la mise en cache concerne uniquement les modules que vous importez. En stockant le bytecode compilé dans les dossiers __pycache__
, Python évite la surcharge de recompilation des modules à chaque exécution de programme, ce qui entraîne des temps de démarrage plus rapides.
Vous comprenez maintenant ce qui déclenche la création de dossiers de cache, comment les supprimer et comment les déplacer vers un dossier centralisé sur votre système de fichiers. En cours de route, vous avez créé un utilitaire pour vous permettre de lire et d'exécuter les fichiers .pyc
individuels à partir d'un dossier __pycache__
.