Was ist der __pycache__-Ordner in Python?
Wenn Sie ein eigenständiges Python-Skript entwickeln, fällt Ihnen möglicherweise nichts Ungewöhnliches an Ihrer Verzeichnisstruktur auf. Sobald Ihr Projekt jedoch komplexer wird, entscheiden Sie sich häufig dafür, Teile der Funktionalität in zusätzliche Module oder Pakete zu extrahieren. Dann bemerken Sie möglicherweise, dass aus dem Nichts neben Ihren Quelldateien an scheinbar zufälligen Stellen ein Ordner __pycache__
erscheint:
project/
│
├── mathematics/
│ │
│ ├── __pycache__/
│ │
│ ├── arithmetic/
│ │ ├── __init__.py
│ │ ├── add.py
│ │ └── sub.py
│ │
│ ├── geometry/
│ │ │
│ │ ├── __pycache__/
│ │ │
│ │ ├── __init__.py
│ │ └── shapes.py
│ │
│ └── __init__.py
│
└── calculator.py
Beachten Sie, dass der Ordner __pycache__
auf verschiedenen Ebenen in der Verzeichnisstruktur Ihres Projekts vorhanden sein kann, wenn Sie mehrere ineinander verschachtelte Unterpakete haben. Gleichzeitig enthalten andere Pakete oder Ordner mit Ihren Python-Quelldateien dieses mysteriöse Cache-Verzeichnis möglicherweise nicht.
Hinweis: Um einen übersichtlicheren Arbeitsbereich zu gewährleisten, sind viele Python-IDEs und Code-Editoren standardmäßig so konfiguriert, dass die Ordner __pycache__
vor Ihnen ausgeblendet werden, selbst wenn diese Ordner vorhanden sind auf Ihrem Dateisystem.
Eine ähnliche Situation kann auftreten, nachdem Sie ein Remote-Git-Repository mit einem Python-Projekt geklont und den zugrunde liegenden Code ausgeführt haben. Was bewirkt also, dass der Ordner __pycache__
angezeigt wird, und zu welchem Zweck?
Kurz gesagt: Es beschleunigt den Import von Python-Modulen
Auch wenn Python eine interpretierte Programmiersprache ist, arbeitet der Interpreter nicht direkt mit Ihrem Python-Code, was sehr langsam wäre. Wenn Sie stattdessen ein Python-Skript ausführen oder ein Python-Modul importieren, kompiliert der Interpreter Ihren Python-Quellcode auf hoher Ebene in Bytecode, der eine binäre Zwischendarstellung des Codes darstellt.
Dieser Bytecode ermöglicht es dem Interpreter, wiederkehrende Schritte zu überspringen, wie z. B. das Lexen und Parsen des Codes in einen abstrakten Syntaxbaum und die Überprüfung seiner Richtigkeit jedes Mal, wenn Sie dasselbe Programm ausführen. Solange sich der zugrunde liegende Quellcode nicht geändert hat, kann Python die Zwischendarstellung wiederverwenden, die sofort zur Ausführung bereit ist. Dies spart Zeit und beschleunigt die Startzeit Ihres Skripts.
Denken Sie daran, dass das Laden des kompilierten Bytecodes von __pycache__
zwar den Import von Python-Modulen beschleunigt, ihre Ausführungsgeschwindigkeit jedoch nicht beeinträchtigt!
Warum sich überhaupt mit Bytecode beschäftigen, anstatt den Code direkt in den Low-Level-Maschinencode zu kompilieren? Während Maschinencode das ist, was auf der Hardware ausgeführt wird und die ultimative Leistung bietet, ist er nicht so portabel oder schnell zu erstellen wie Bytecode.
Maschinencode ist ein Satz binärer Anweisungen, die von Ihrer spezifischen CPU-Architektur verstanden werden und je nach Betriebssystem in ein Containerformat wie EXE, ELF oder Mach-O verpackt sind. Im Gegensatz dazu bietet Bytecode eine plattformunabhängige Abstraktionsschicht und ist in der Regel schneller zu kompilieren.
Python verwendet lokale __pycache__
-Ordner, um den kompilierten Bytecode importierter Module in Ihrem Projekt zu speichern. Bei nachfolgenden Durchläufen wird der Interpreter versuchen, vorkompilierte Versionen von Modulen aus diesen Ordnern zu laden, sofern sie mit den entsprechenden Quelldateien auf dem neuesten Stand sind. Beachten Sie, dass dieser Caching-Mechanismus nur für Module ausgelöst wird, die Sie in Ihren Code importieren und nicht als Skripte im Terminal ausführen.
Zusätzlich zu diesem Bytecode-Caching auf der Festplatte behält Python einen In-Memory-Cache von Modulen bei, auf den Sie über sys.modules
zugreifen können Wörterbuch. Dadurch wird sichergestellt, dass Python das bereits importierte Modul verwendet, wenn Sie dasselbe Modul mehrmals von verschiedenen Stellen in Ihrem Programm importieren, ohne es neu laden oder neu kompilieren zu müssen. Beide Mechanismen arbeiten zusammen, um den Aufwand beim Importieren von Python-Modulen zu reduzieren.
Als Nächstes erfahren Sie genau, wie viel schneller Python den zwischengespeicherten Bytecode lädt, als wenn Sie den Quellcode beim Importieren eines Moduls spontan kompilieren.
Wie viel schneller ist das Laden von Modulen aus dem Cache?
Das Caching geschieht im Verborgenen und bleibt normalerweise unbemerkt, da Python den Bytecode recht schnell kompiliert. Außerdem bleibt der Kompilierungsschritt im Vergleich zur Gesamtausführungszeit unbedeutend, es sei denn, Sie führen häufig kurzlebige Python-Skripte aus. Allerdings könnte sich der mit der Bytecode-Kompilierung verbundene Overhead ohne Caching summieren, wenn Sie viele Module hätten und diese viele Male importieren würden.
Um den Unterschied in der Importzeit zwischen einem zwischengespeicherten und einem nicht zwischengespeicherten Modul zu messen, können Sie die Option -X importtime
an den Befehl python
übergeben oder festlegen entsprechende Umgebungsvariable PYTHONPROFILEIMPORTTIME
. Wenn diese Option aktiviert ist, zeigt Python eine Tabelle an, die zusammenfasst, wie lange es gedauert hat, jedes Modul zu importieren, einschließlich der Gesamtzeit für den Fall, dass ein Modul von anderen Modulen abhängt.
Angenommen, Sie hätten ein calculator.py
-Skript, das eine Hilfsfunktion aus einem lokalen arithmetic.py
-Modul importiert und aufruft:
from arithmetic import add
add(3, 4)
Das importierte Modul definiert eine einzelne Funktion:
def add(a, b):
return a + b
Wie Sie sehen können, delegiert das Hauptskript die Addition von zwei Zahlen, drei und vier, an die Funktion add()
, die aus dem Modul arithmetic
importiert wurde.
Hinweis: Auch wenn Sie die Syntax from ... import
verwenden, die nur das angegebene Symbol in Ihren aktuellen Namespace bringt, liest und kompiliert Python trotzdem das gesamte Modul. Darüber hinaus würden auch ungenutzte Importe die Kompilierung auslösen.
Wenn Sie Ihr Skript zum ersten Mal ausführen, kompiliert Python den Bytecode des von Ihnen importierten Moduls und speichert ihn in einem lokalen Ordner __pycache__
. Wenn ein solcher Ordner noch nicht vorhanden ist, erstellt Python automatisch einen, bevor es fortfährt. Wenn Sie nun Ihr Skript erneut ausführen, sollte Python den zwischengespeicherten Bytecode finden und laden, sofern Sie den zugehörigen Quellcode nicht geändert haben.
Sobald der Cache aufgewärmt ist, trägt er zu einer schnelleren Startzeit Ihres Python-Skripts bei:
$ 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
Obwohl die genauen Messwerte von Lauf zu Lauf variieren können, ist die Verbesserung deutlich sichtbar. Ohne den Ordner __pycache__
war der erste Versuch, arithmetic
zu importieren, zwei Größenordnungen langsamer als bei nachfolgenden Durchläufen. Diese Veränderung mag auf den ersten Blick verblüffend erscheinen, aber diese Werte werden in Mikrosekunden ausgedrückt, sodass Sie den Unterschied trotz eines so dramatischen Rückgangs der Zahlen höchstwahrscheinlich nicht einmal bemerken werden.
Der Leistungsgewinn spiegelt sich in der Regel kaum in der Startzeit der meisten Python-Skripte wider, die Sie auf Unix-ähnlichen Systemen mit dem Befehl time
abschätzen können:
$ 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
Dabei bleibt die Gesamtausführungszeit nahezu gleich, unabhängig davon, ob der Cache vorhanden ist oder nicht. Das Entfernen des Ordners __pycache__
verzögert die Ausführung um etwa zwei Millisekunden, was für die meisten Anwendungen vernachlässigbar ist.
Der Bytecode-Compiler von Python ist ziemlich schnell, wenn man ihn mit einem anspruchsvolleren Gegenstück in Java vergleicht, das die statische Typisierung nutzen kann.
Wenn Sie beispielsweise über eine Beispieldatei Calculator.java
verfügen, können Sie diese entweder im Voraus in eine Datei .class
kompilieren, was der üblichen Arbeitsweise mit Java-Code entspricht , oder führen Sie die Datei .java
direkt aus. Im letzteren Fall kompiliert die Java-Laufzeitumgebung den Code im Hintergrund an einem temporären Speicherort, bevor er ausgeführt wird:
$ 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
Wenn Sie den Quellcode manuell mit dem Befehl javac
kompilieren und den resultierenden Bytecode ausführen, dauert die Ausführung etwa vierzig Millisekunden. Wenn Sie die Kompilierung hingegen dem Befehl java
überlassen, erhöht sich die Gesamtausführungszeit auf etwas mehr als eine halbe Sekunde. Daher ist der Overhead des Java-Compilers im Gegensatz zu Python selbst bei einer parallelen Kompilierung auf mehreren CPU-Kernen sehr spürbar!
Nachdem Sie nun den Zweck des Ordners __pycache__
kennen, sind Sie vielleicht neugierig auf seinen Inhalt.
Was befindet sich in einem __pycache__
-Ordner?
Wenn Sie einen Blick in den Ordner __pycache__
werfen, sehen Sie eine oder mehrere Dateien, die mit der Erweiterung .pyc
enden. Es steht für ein kompiliertes Python-Modul:
$ 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
Jede dieser Dateien enthält den Bytecode des entsprechenden Python-Moduls, das im aktuellen Paket definiert ist und das Sie zur Laufzeit importiert haben. Der kompilierte Bytecode zielt auf eine bestimmte Python-Implementierung, -Version und eine optionale Optimierungsstufe ab. Alle diese Informationen sind im Dateinamen verschlüsselt.
Beispielsweise ist eine Datei namens arithmetic.pypy310.opt-2.pyc
der Bytecode Ihres arithmetic.py
-Moduls, das von PyPy 3.10 mit einer Optimierungsstufe von zwei kompiliert wurde. Diese Optimierung entfernt die assert
-Anweisungen und verwirft alle Dokumentzeichenfolgen. Umgekehrt stellt arithmetic.cpython-312.pyc
dasselbe Modul dar, das jedoch ohne Optimierungen für CPython 3.12 kompiliert wurde.
Insgesamt gibt es fünf Bytecode-Varianten, die aus einem einzigen Modul arithmetic.py
im Ordner __pycache__
oben kompiliert wurden und hervorgehoben sind.
Hinweis: Vor Python 3.5 führte die Aktivierung einer der Optimierungsstufen mit dem Flag -O
oder -OO
dazu, dass der Bytecode in einen separaten .pyo
-Datei im Cache-Ordner. Dies änderte sich nach Inkrafttreten von PEP 488 und die optionale Optimierungsstufe ist jetzt im Dateinamen .pyc
kodiert.
Ein solches Dateibenennungsschema gewährleistet die Kompatibilität zwischen verschiedenen Python-Versionen und -Varianten. Wenn Sie dasselbe Skript mit PyPy oder einer früheren CPython-Version ausführen, kompiliert der Interpreter alle importierten Module in seiner eigenen Laufzeitumgebung, sodass er sie später wiederverwenden kann. Die Python-Version wird zusammen mit anderen Metadaten auch in der Datei .pyc
selbst gespeichert.
Am Ende dieses Tutorials werden Sie sich die im Cache-Ordner gespeicherten .pyc
-Dateien genauer ansehen. Jetzt ist es an der Zeit, mehr über die Umstände zu erfahren, die Python dazu veranlassen, den Cache-Ordner zu erstellen.
Wann erstellt Python Cache-Ordner?
Der Interpreter speichert den kompilierten Bytecode von Python-Modulen nur dann im Ordner __pycache__
, wenn Sie diese Module oder manchmal ihr übergeordnetes Paket importieren. Der Cache-Ordner wird nicht erstellt, wenn Sie ein normales Python-Skript ausführen, das keine Module oder Pakete importiert. Dies basiert auf der Annahme, dass sich Module weniger wahrscheinlich ändern und dass Sie sie während einer einzelnen Ausführung möglicherweise mehrmals importieren.
Hinweis: Obwohl Sie der Bytecode-Kompilierung nur über den Ordner __pycache__
ausgesetzt sind, wenn Sie lokale Module oder Pakete importieren, funktioniert die zugrunde liegende Maschinerie genauso gut Pakete von Drittanbietern und Standardbibliotheken.
Wenn Sie ein Paket eines Drittanbieters in einer virtuellen Umgebung installieren, kompiliert pip
standardmäßig die Module des Pakets in .pyc
-Dateien mit dem Python-Interpreter, den es gerade ausführt An. Ebenso sind die reinen Python-Module in der Standardbibliothek von Python mit allen möglichen Optimierungsstufen vorkompiliert.
Wenn Sie eine dieser .pyc
-Dateien entfernen würden, entweder aus dem Ordner site-packages/
der virtuellen Umgebung oder dem Ordner lib/
von Python, dann würde der Interpreter es neu erstellen, wenn Sie das entsprechende Modul das nächste Mal importieren.
Wenn Sie ein einzelnes Modul aus einem Paket importieren, erstellt Python die entsprechende .pyc
-Datei und speichert sie im Ordner __pycache__
, der sich innerhalb dieses Pakets befindet . Außerdem wird die Datei __init__.py
des Pakets kompiliert, andere Module oder verschachtelte Unterpakete werden jedoch nicht berührt. Wenn jedoch das importierte Modul selbst andere Module importiert, werden diese Module ebenfalls kompiliert und so weiter.
Hier ist ein Beispiel, das den einfachsten Fall demonstriert, vorausgesetzt, Sie haben eine geeignete import
-Anweisung in das folgende Skript calculator.py
eingefügt:
project/
│
├── arithmetic/
│ │
│ ├── __pycache__/
│ │ ├── __init__.cpython-312.pyc
│ │ └── add.cpython-312.pyc
│ │
│ ├── __init__.py
│ ├── add.py
│ └── sub.py
│
└── calculator.py
Nach der ersten Ausführung des Skripts stellt Python sicher, dass im Paket arithmetic
ein Ordner __pycache__
vorhanden ist. Anschließend wird die Datei __init__.py
des Pakets zusammen mit allen aus diesem Paket importierten Modulen kompiliert. In diesem Fall haben Sie nur den Import des Moduls arithmetic.add
angefordert, sodass Sie eine .pyc
-Datei sehen können, die mit add.py
verknüpft ist nicht mit sub.py
.
Alle folgenden import
-Anweisungen würden das gleiche oben dargestellte Ergebnis liefern:
import arithmetic.add
from arithmetic import add
from arithmetic.add import add_function
Unabhängig davon, wie Sie ein Python-Modul importieren und ob Sie es vollständig oder nur ein bestimmtes Symbol, beispielsweise eine Klasse oder eine Konstante, importieren, kompiliert der Interpreter das gesamte Modul, da er Module nicht teilweise lesen kann.
Umgekehrt würde das Importieren eines ganzen Pakets normalerweise dazu führen, dass Python nur __init__.py
in diesem Paket kompiliert. Es ist jedoch durchaus üblich, dass Pakete ihre internen Module oder Unterpakete aus __init__.py
heraus zugänglich machen, um einen bequemeren Zugriff zu ermöglichen. Bedenken Sie zum Beispiel Folgendes:
from arithmetic import add
from arithmetic.sub import sub_function
Importe innerhalb von __init__.py
wie diese erstellen zusätzliche .pyc
-Dateien, selbst wenn Sie die einfache import arithmetic
-Anweisung in Ihrem Skript verwenden.
Wenn Sie ein Unterpaket oder ein tief verschachteltes Modul oder Symbol importiert haben, werden für alle Zwischenpakete, die zum Paket der obersten Ebene führen, auch die Dateien __init__.py
kompiliert und platziert in ihren jeweiligen Cache-Ordnern. Allerdings wird Python nicht in die andere Richtung gehen und die verschachtelten Unterpakete rekursiv scannen, da dies unnötig wäre. Es kompiliert nur die Module, die Sie wirklich benötigen, indem es sie explizit oder indirekt importiert.
Hinweis: Der bloße Vorgang des Importierens eines Pakets oder Moduls löst die Kompilierung aus. Sie können ungenutzte Importanweisungen in Ihrem Code haben, und Python wird sie trotzdem kompilieren!
Was passiert, wenn Python Ihr Modul bereits in eine .pyc
-Datei kompiliert hat, Sie sich aber dazu entschließen, seinen Quellcode in der ursprünglichen .py
-Datei zu ändern? Das erfahren Sie als Nächstes!
Welche Aktionen machen den Cache ungültig?
Das Ausführen eines veralteten Bytecodes könnte einen Fehler verursachen oder, schlimmer noch, zu völlig unvorhersehbarem Verhalten führen. Glücklicherweise ist Python clever genug, um zu erkennen, wenn Sie den Quellcode eines kompilierten Moduls ändern, und es bei Bedarf neu zu kompilieren.
Um festzustellen, ob ein Modul neu kompiliert werden muss, verwendet Python eine von zwei Cache-Invalidierungsstrategien:
- Zeitstempelbasiert
- Hash-basiert
Der erste vergleicht die Größe der Quelldatei und ihren Zeitstempel der letzten Änderung mit den Metadaten, die in der entsprechenden .pyc
-Datei gespeichert sind. Später erfahren Sie, wie diese Werte zusammen mit anderen Metadaten in der Datei .pyc
gespeichert werden.
Im Gegensatz dazu berechnet die zweite Strategie den Hash-Wert der Quelldatei und vergleicht ihn mit einem speziellen Feld im Header der Datei (PEP 552), das in Python 3.7 eingeführt wurde. Diese Strategie ist sicherer und deterministischer, aber auch etwas langsamer. Aus diesem Grund bleibt die zeitstempelbasierte Strategie vorerst die Standardstrategie.
Wenn Sie die Änderungszeit (mtime
) der Quelldatei künstlich aktualisieren, beispielsweise durch Verwendung des Befehls touch
unter macOS oder Linux, zwingen Sie Python, die Datei zu kompilieren Modul noch einmal:
$ 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
Ursprünglich wurde die zwischengespeicherte Datei arithmetic.cpython-312.pyc
zuletzt um 09:48 Uhr geändert. Nach dem Berühren der Quelldatei arithmetic.py
hält Python den kompilierten Bytecode für veraltet und kompiliert das Modul neu, wenn Sie das Skript ausführen, das arithmetic
importiert. Dies führt zu einer neuen .pyc
-Datei mit einem aktualisierten Zeitstempel von 09:52 Uhr.
Um Hash-basierte .pyc
-Dateien zu erstellen, müssen Sie das compileall
-Modul von Python mit der entsprechend eingestellten Option --invalidation-mode
verwenden. Dieser Befehl kompiliert beispielsweise alle Module im aktuellen Ordner und den Unterordnern in der sogenannten checked Hash-basierten Variante:
$ python -m compileall --invalidation-mode checked-hash
Die offizielle Dokumentation erklärt den Unterschied zwischen geprüften und ungeprüften Varianten der Hash-basierten .pyc
-Dateien wie folgt:
Bei überprüften Hash-basierten
.pyc
-Dateien validiert Python die Cache-Datei, indem es die Quelldatei hasht und den resultierenden Hash mit dem Hash in der Cache-Datei vergleicht. Wenn sich herausstellt, dass eine überprüfte Hash-basierte Cache-Datei ungültig ist, generiert Python sie neu und schreibt eine neue überprüfte Hash-basierte Cache-Datei. Bei ungeprüften Hash-basierten.pyc
-Dateien geht Python einfach davon aus, dass die Cache-Datei gültig ist, sofern sie existiert. (Quelle)
Dennoch können Sie das Standardvalidierungsverhalten hashbasierter .pyc
-Dateien jederzeit mit der Option --check-hash-based-pycs
überschreiben, wenn Sie den Python-Interpreter ausführen.
Wenn Sie wissen, wann und wo Python die Ordner __pycache__
erstellt und wann deren Inhalt aktualisiert wird, erhalten Sie eine Vorstellung davon, ob es sicher ist, sie zu entfernen.
Ist es sicher, einen Cache-Ordner zu entfernen?
Ja, obwohl Sie sich fragen sollten, ob Sie das wirklich sollten! An diesem Punkt verstehen Sie, dass das Entfernen eines Ordners __pycache__
harmlos ist, da Python den Cache bei jedem Aufruf neu generiert. Ohnehin ist das manuelle Entfernen der einzelnen Cache-Ordner eine mühsame Arbeit. Außerdem dauert es nur bis zur nächsten Ausführung Ihres Codes.
Hinweis: Während Sie die Cache-Ordner in den meisten Fällen nicht entfernen müssen, empfiehlt es sich, sie von einem Versionskontrollsystem wie Git auszuschließen, indem Sie beispielsweise ein entsprechendes Dateimuster zu Ihrem Projekt hinzufügen .gitignore
. Wenn Ihr Code-Editor dies noch nicht getan hat, können Sie ihn außerdem so konfigurieren, dass solche Ordner vor dem Datei-Explorer ausgeblendet werden, um Ablenkungen zu vermeiden.
Die gute Nachricht ist, dass Sie das Entfernen dieser Cache-Ordner aus Ihrem Projekt automatisieren können, wenn Sie wirklich darauf bestehen, was Sie jetzt tun werden.
Wie entferne ich alle Cache-Ordner rekursiv?
Okay, Sie haben bereits festgestellt, dass das Entfernen des zwischengespeicherten Bytecodes, den Python kompiliert, keine große Sache ist. Das Problem mit diesen __pycache__
-Ordnern besteht jedoch darin, dass sie in mehreren Unterverzeichnissen erscheinen können, wenn Sie eine komplexe Projektstruktur haben. Sie manuell zu finden und zu löschen wäre mühsam, vor allem, da sie jedes Mal, wenn Sie Python ausführen, wie ein Phönix aus der Asche auferstehen.
Unten finden Sie die plattformspezifischen Befehle, die alle __pycache__
-Ordner auf einmal rekursiv aus dem aktuellen Verzeichnis und allen darin verschachtelten Unterverzeichnissen entfernen:
Das erste Code-Snippet sollte unter Windows verwendet werden und das zweite Code-Snippet ist für 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 {} +
Sie sollten bei der Ausführung von Massenlöschbefehlen Vorsicht walten lassen, da dadurch mehr gelöscht werden kann, als Sie beabsichtigt haben, wenn Sie nicht vorsichtig sind. Überprüfen Sie immer die Pfade und Filterkriterien, bevor Sie solche Befehle ausführen!
Das Löschen der Ordner __pycache__
kann den Arbeitsbereich Ihres Projekts aufräumen, allerdings nur vorübergehend. Wenn es Sie immer noch nervt, dass Sie den rekursiven Löschbefehl wiederholt ausführen müssen, möchten Sie möglicherweise zunächst die Kontrolle über die Verwaltung des Cache-Ordners übernehmen. Als Nächstes erkunden Sie zwei Ansätze zur Lösung dieses Problems.
Wie kann verhindert werden, dass Python Cache-Ordner erstellt?
Wenn Sie nicht möchten, dass Python den kompilierten Bytecode zwischenspeichert, können Sie beim Ausführen eines Skripts die Option -B
an den Befehl python
übergeben. Dadurch wird verhindert, dass die Ordner __pycache__
angezeigt werden, es sei denn, sie sind bereits vorhanden. Allerdings wird Python weiterhin alle .pyc
-Dateien nutzen, die es in vorhandenen Cache-Ordnern finden kann. Es werden einfach keine neuen Dateien auf die Festplatte geschrieben.
Für einen dauerhafteren und globalen Effekt, der sich über mehrere Python-Interpreter erstreckt, können Sie die Umgebungsvariable PYTHONDONTWRITEBYTECODE
in Ihrer Shell oder deren Konfigurationsdatei festlegen:
export PYTHONDONTWRITEBYTECODE=1
Es wirkt sich auf jeden Python-Interpreter aus, auch auf einen in einer von Ihnen aktivierten virtuellen Umgebung.
Dennoch sollten Sie sorgfältig darüber nachdenken, ob die Unterdrückung der Bytecode-Kompilierung für Ihren Anwendungsfall der richtige Ansatz ist. Die Alternative besteht darin, Python anzuweisen, die einzelnen __pycache__
-Ordner an einem einzigen freigegebenen Speicherort in Ihrem Dateisystem zu erstellen.
Wie speichere ich den Cache in einem zentralen Ordner?
Wenn Sie die Bytecode-Kompilierung vollständig deaktivieren, erhalten Sie einen aufgeräumteren Arbeitsbereich, verlieren jedoch die Vorteile des Cachings für schnellere Ladezeiten. Wenn Sie das Beste aus beiden Welten kombinieren möchten, können Sie Python mit dem -X pycache_prefix.pyc
-Dateien in einen parallelen Baum zu schreiben, der im angegebenen Verzeichnis verwurzelt ist. Code> Option:
$ python -X pycache_prefix=/tmp/pycache calculator.py
In diesem Fall weisen Sie Python an, den kompilierten Bytecode in einem temporären Ordner unter /tmp/pycache
in Ihrem Dateisystem zwischenzuspeichern. Wenn Sie diesen Befehl ausführen, versucht Python nicht mehr, lokale __pycache__
-Ordner in Ihrem Projekt zu erstellen. Stattdessen wird die Verzeichnisstruktur Ihres Projekts im angegebenen Stammordner gespiegelt und alle .pyc
-Dateien dort gespeichert:
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
Beachten Sie hier zwei Dinge. Erstens besteht keine Notwendigkeit, die kompilierten .pyc
-Dateien in den __pycache__
-Ordnern zu verschachteln, da das Cache-Verzeichnis von Ihrem Quellcode getrennt gehalten wird. Zweitens können Sie diesen Cache-Ordner zwischen mehreren Projekten teilen, da die Hierarchie innerhalb eines solchen zentralen Caches mit der Struktur Ihres Projekts übereinstimmt.
Zu den weiteren Vorteilen dieses Setups gehört eine einfachere Bereinigung, da Sie alle .pyc
-Dateien, die zum selben Projekt gehören, mit einem einzigen Tastendruck entfernen können, ohne alle Verzeichnisse manuell durchsuchen zu müssen. Darüber hinaus können Sie den Cache-Ordner auf einer separaten physischen Festplatte speichern, um parallele Lesevorgänge zu nutzen, oder den Cache in einem persistenten Volume aufbewahren, wenn Sie mit Docker-Containern arbeiten.
Denken Sie daran, dass Sie die Option -X pycache_prefix
jedes Mal verwenden müssen, wenn Sie den Befehl python
ausführen, damit dies konsistent funktioniert. Alternativ können Sie den Pfad zu einem freigegebenen Cache-Ordner über die Umgebungsvariable PYTHONPYCACHEPREFIX
festlegen:
export PYTHONPYCACHEPREFIX=/tmp/pycache
In beiden Fällen können Sie programmgesteuert überprüfen, ob Python das angegebene Cache-Verzeichnis verwendet oder auf das Standardverhalten zurückgreift und lokale __pycache__
-Ordner erstellt:
>>> import sys
>>> sys.pycache_prefix
'/tmp/pycache'
Die Variable sys.pycache_prefix
kann entweder eine Zeichenfolge oder Keine
sein.
Sie haben mit diesem Tutorial einen langen Weg zurückgelegt und wissen jetzt ein oder zwei Dinge über den Umgang mit __pycache__
-Ordnern in Ihren Python-Projekten. Endlich ist es an der Zeit, zu sehen, wie Sie direkt mit den in diesen Ordnern gespeicherten .pyc
-Dateien arbeiten können.
Was befindet sich in einer zwischengespeicherten .pyc
-Datei?
Eine .pyc
-Datei besteht aus einem Header mit Metadaten, gefolgt vom serialisierten Codeobjekt, das zur Laufzeit ausgeführt werden soll. Der Header der Datei beginnt mit einer magischen Zahl, die die spezifische Python-Version, für die der Bytecode kompiliert wurde, eindeutig identifiziert. Als nächstes gibt es ein in PEP 552 definiertes Bitfeld, das eine der drei zuvor erläuterten Cache-Invalidierungsstrategien bestimmt.
In zeitstempelbasierten .pyc
-Dateien wird das Bitfeld mit Nullen gefüllt, gefolgt von zwei Vier-Byte-Feldern. Diese Felder entsprechen der Unix-Zeit der letzten Änderung bzw. der Größe der Quelldatei .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 |
Umgekehrt kann das Bitfeld bei Hash-basierten .pyc
-Dateien entweder gleich eins sein, was eine ungeprüfte Variante anzeigt, oder drei, was bedeutet die geprüfte Variante. Dann gibt es statt des Zeitstempels und der Dateigröße nur ein acht Byte langes Feld mit dem Hash-Wert des Python-Quellcodes:
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 |
In beiden Fällen ist der Header sechzehn Bytes lang. Sie können ihn überspringen, wenn Sie nicht daran interessiert sind, die codierten Metadaten zu lesen. Auf diese Weise springen Sie direkt zu dem mit dem Modul marshal
serialisierten Codeobjekt, das den verbleibenden Teil der Datei .pyc
einnimmt.
Mit diesen Informationen können Sie eine Ihrer kompilierten .pyc
-Dateien durchleuchten und den zugrunde liegenden Bytecode direkt ausführen, auch wenn Sie nicht mehr über die ursprüngliche .py
-Datei verfügen zugehörigen Quellcode.
Wie lese und führe ich den zwischengespeicherten Bytecode aus?
Python selbst führt nur dann eine begleitende .pyc
-Datei aus, wenn die ursprüngliche .py
-Datei noch vorhanden ist. Wenn Sie das Quellmodul entfernen, nachdem es bereits kompiliert wurde, weigert sich Python, die Datei .pyc
auszuführen. Das ist beabsichtigt. Sie können den Bytecode jedoch jederzeit manuell ausführen, wenn Sie möchten.
Hinweis: Wenn Sie dies in ferner Zukunft lesen, besteht die Möglichkeit, dass sich das zugrunde liegende Dateiformat .pyc
oder die Standardbibliotheks-API geändert hat. Daher muss das Skript, das Sie gleich sehen werden, möglicherweise noch angepasst werden, bevor es mit der aktuellen Python-Version wie vorgesehen funktioniert. Alternativ können Sie es mit Python 3.12 ausführen, um die Kompatibilität sicherzustellen.
Das folgende Python-Skript zeigt, wie Sie den Header der Datei .pyc
lesen und den dazugehörigen Bytecode deserialisieren und ausführen können:
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])
Hier passieren mehrere Dinge, die Sie Zeile für Zeile aufschlüsseln können:
- Zeile 11 empfängt ein Tupel, das die aus dem Header der Datei analysierten Metadaten und ein deserialisiertes Codeobjekt enthält, das zur Ausführung bereit ist. Beide werden aus einer
.pyc
-Datei geladen, die als einziges erforderliches Befehlszeilenargument angegeben ist. - Zeile 12 gibt die entschlüsselten Metadaten hübsch auf dem Bildschirm aus.
- Zeilen 13 bis 16 führen den Bytecode aus der Datei
.pyc
bedingt mitexec()
aus oder geben eine Fehlermeldung aus. Um festzustellen, ob die Datei für die aktuelle Interpreterversion kompiliert wurde, vergleicht dieses Codefragment die aus dem Header erhaltene magische Zahl mit der magischen Zahl des Interpreters. Wenn alles gelingt, werden die Symbole des geladenen Moduls inglobals()
importiert. - Zeilen 18 bis 23 öffnen die Datei
.pyc
im Binärmodus mit dem Modulpathlib
, analysieren den Header und entmarshalieren das Codeobjekt. - Zeilen 25 bis 44 analysieren die Header-Felder unter Verwendung ihrer entsprechenden Offsets und Bytegrößen und interpretieren Multibyte-Werte mit der Little-Endian-Bytereihenfolge.
- Zeilen 28 und 29 extrahieren die Python-Version aus der magischen Zahl, die mit jeder Nebenversion von Python gemäß der Formel 2900 + 50n erhöht wird, wobei n die Nebenversion von Python 3.11 oder höher ist.
- Zeilen 31 bis 35 bestimmen den Typ der
.pyc
-Datei basierend auf dem vorhergehenden Bitfeld (PEP 552). - Zeilen 37 bis 40 konvertieren die Änderungszeit der Quelldatei in ein
datetime
-Objekt in der UTC-Zeitzone.
Sie können das oben beschriebene Röntgenskript für eine .pyc
-Datei Ihrer Wahl ausführen. Wenn Sie den interaktiven Modus von Python aktivieren (-i
), können Sie die Variablen und den Status des Programms nach dessen Beendigung überprüfen:
$ 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
Das Skript gibt die dekodierten Header-Felder aus, einschließlich der magischen Zahl und der Änderungszeit der Quelldatei. Unmittelbar danach gelangen Sie aufgrund der Option python -i
in die interaktive Python-REPL, wo Sie add()
aufrufen, die von in den globalen Namespace importiert wurde Ausführen des Bytecodes des Moduls.
Dies funktioniert wie erwartet, da der Python-Interpreter, den Sie gerade ausführen, zufällig mit der Bytecode-Version des Moduls übereinstimmt. Folgendes würde passieren, wenn Sie versuchen würden, eine andere .pyc
-Datei auszuführen, die auf eine andere Python-Version oder eine ihrer alternativen Implementierungen abzielt:
$ 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
Dieses Mal zeigt die Ausgabe eine Meldung an, die auf eine Nichtübereinstimmung zwischen der Bytecode-Version in der Datei .pyc
und dem verwendeten Interpreter hinweist. Infolgedessen wurde der Bytecode nicht ausgeführt und die Funktion add()
wurde nicht definiert, sodass Sie sie nicht aufrufen können.
Wenn Sie nun eine Hash-basierte .pyc
-Datei (aktiviert oder deaktiviert) durchleuchten, erhalten Sie möglicherweise Folgendes:
$ 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 kann den in der Datei .pyc
eingebetteten Hash-Wert mit einem vergleichen, den es aus der zugehörigen Datei .py
berechnet, indem es source_hash()
aufruft der Quellcode:
>>> from importlib.util import source_hash
>>> from pathlib import Path
>>> source_hash(Path("arithmetic.py").read_bytes())
b'\xf3\xdd\x87j\x8d>\x0e)'
Dies ist eine zuverlässigere Methode zur Cache-Ungültigmachung und Überprüfung der Codeintegrität als der Vergleich eines flüchtigen, zuletzt geänderten Attributs der Quelldatei. Beachten Sie, dass der berechnete Hash-Wert mit dem aus der Datei .pyc
gelesenen übereinstimmt.
Nachdem Sie nun wissen, wie Sie Python-Module aus einer kompilierten Binärform importieren, könnte es verlockend sein, Ihre kommerziellen Python-Programme zu verbreiten, ohne den Quellcode weiterzugeben.
Kann Bytecode Python-Programme verschleiern?
Zuvor haben Sie erfahren, dass Python ein Modul nicht aus einer .pyc
-Datei importiert, wenn die zugehörige .py
-Datei nicht gefunden werden kann. Es gibt jedoch eine bemerkenswerte Ausnahme, da genau das passiert, wenn Sie Python-Code aus einer ZIP-Datei importieren, die im PYTHONPATH
angegeben ist. Solche Archive enthalten normalerweise nur die kompilierten .pyc
-Dateien ohne den zugehörigen Quellcode.
Durch die Möglichkeit, kompilierte Module entweder manuell oder über diese ZIP-Dateien zu importieren, können Sie einen rudimentären Code-Verschleierungsmechanismus implementieren. Leider wäre es nicht besonders kugelsicher, da technisch versiertere Benutzer versuchen könnten, Ihre .pyc
-Dateien mit speziellen Tools wie uncompyle6
oder wieder in Python-Code auf hoher Ebene zu dekompilieren pycdc
.
Aber auch ohne diese externen Tools können Sie den Bytecode von Python in für Menschen lesbare Opcodes zerlegen und so die Analyse und das Reverse Engineering Ihrer Programme relativ einfach gestalten. Der richtige Weg, Python-Quellcode zu verbergen, besteht darin, ihn in Maschinencode zu kompilieren. Sie können sich beispielsweise mit Tools wie Cython helfen oder die Kernteile Ihres Codes in einer niedrigeren Programmiersprache wie C, C++ oder Rust neu schreiben.
Wie zerlege ich den zwischengespeicherten Bytecode?
Sobald Sie ein Codeobjekt in Python im Griff haben, können Sie das Modul dis
aus der Standardbibliothek verwenden, um den kompilierten Bytecode zu zerlegen. Beispielsweise generieren Sie schnell selbst ein Codeobjekt, ohne sich auf die von Python zwischengespeicherten .pyc
-Dateien verlassen zu müssen:
>>> 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>
Sie rufen die integrierte Funktion compile()
mit dem Modus "exec"
als Parameter auf, um ein Python-Modul zu kompilieren. Jetzt können Sie mit dis
die für Menschen lesbaren Opcode-Namen des resultierenden Codeobjekts anzeigen:
>>> 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
Die Opcodes MAKE_FUNCTION
und STORE_NAME
sagen Ihnen, dass es in diesem Bytecode eine Funktion namens add()
gibt. Wenn Sie sich das zerlegte Codeobjekt dieser Funktion genau ansehen, werden Sie feststellen, dass es zwei Argumente namens a
und b
benötigt und diese mithilfe des binären Plusoperators (+
) und gibt den berechneten Wert zurück.
Alternativ können Sie durch den Opcode-Baum gehen und die einzelnen Anweisungen durchlaufen, um zu versuchen, den Python-Quellcode auf hoher Ebene zu rekonstruieren:
>>> 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')
Dieser Codeausschnitt ist noch lange nicht vollständig. Außerdem ist die Dekompilierung im Allgemeinen ein komplizierter Prozess. Dies führt oft zu unvollständigen Ergebnissen, da einige Informationen irreversibel verloren gehen, wenn bestimmte Optimierungen während der Bytecode-Kompilierung angewendet werden. Auf jeden Fall sollte es Ihnen eine ungefähre Vorstellung davon geben, wie Dekompilierer funktionieren.
Abschluss
In diesem Tutorial haben Sie sich mit der Funktionsweise des Bytecode-Caching-Mechanismus von Python befasst. Sie verstehen jetzt, dass es beim Caching ausschließlich um die Module geht, die Sie importieren. Durch das Speichern von kompiliertem Bytecode in __pycache__
-Ordnern vermeidet Python den Aufwand, Module bei jedem Programmlauf neu zu kompilieren, was zu schnelleren Startzeiten führt.
Jetzt verstehen Sie, was die Erstellung von Cache-Ordnern auslöst, wie Sie diese unterdrücken und wie Sie sie in einen zentralen Ordner in Ihrem Dateisystem verschieben. Nebenbei haben Sie ein Hilfsprogramm erstellt, mit dem Sie die einzelnen .pyc
-Dateien aus einem __pycache__
-Ordner lesen und ausführen können.