Python __init__.py, mehr als nur eine leere Datei



Viele Python-Entwickler nutzen die Datei __init__.py nur als leere Datei, um "dieses Verzeichnis als Paket zu erkennen". Aber diese Datei ist nicht einfach nur ein Marker.

Der Kern von __init__.py ist der „Das Skript, das immer zuerst ausgeführt wird, wenn das Paket importiert wird“.

Durch die Nutzung dieser Eigenschaft kann man ordentlicheren Code schreiben, die API des Pakets klarer gestalten und die Initialisierungslogik verwalten, was sehr mächtige Aufgaben ermöglicht.


1. Unordentliche Import-Pfade aufräumen (API vereinfachen)

Dies ist die nützlichste und gängigste Verwendung von __init__.py. Hierbei werden Klassen oder Funktionen von Untermodulen auf Paket-Ebene „nach oben gezogen“, um es den Benutzern zu erleichtern, sie zu importieren.

Problem:

Angenommen, die views.py von Django hat mehr als 1.000 Zeilen und wurde nach Funktionen in verschiedene Dateien unterteilt.

payments/
├── views/
│   ├── __init__.py
│   ├── user_views.py    # UserProfileView, ...
│   ├── payment_views.py # PaymentView, ...
│   └── webhooks.py      # PaypalWebhookView, ...
└── urls.py

Es ist sehr umständlich, diese Views in urls.py zu verwenden.

# payments/urls.py (schlechtes Beispiel)
from .views import user_views, payment_views, webhooks

urlpatterns = [
    path('profile/', user_views.UserProfileView.as_view()),
    path('toss/', payment_views.PaymentView.as_view()),
    path('webhook/', webhooks.PaypalWebhookView.as_view()),
]

urls.py muss die gesamte interne Struktur des views-Verzeichnisses kennen.


Lösung (init.py Verwendung):

Im init.py der views werden die benötigten Views im Voraus importiert.

# payments/views/__init__.py

# Die notwendigen Klassen/Funktionen werden auf Paket-Ebene nach oben gezogen.
from .user_views import UserProfileView
from .payment_views import PaymentView
from .webhooks import PaypalWebhookView

Jetzt sieht es für urls.py, das das views-Paket verwendet, so aus, als ob das Paket UserProfileView direkt besäße.

# payments/urls.py (verbessertes Beispiel)

# 'views' ist jetzt ein Paket (Verzeichnis) und kein einzelnes Datei.
from . import views

urlpatterns = [
    # Dank __init__.py muss die interne Struktur nicht bekannt sein.
    path('profile/', views.UserProfileView.as_view()),
    path('toss/', views.PaymentView.as_view()),
    path('webhook/', views.PaypalWebhookView.as_view()),
]

Der Code für urls.py bleibt gleich, egal ob views.py eine Datei oder ein Verzeichnis mit mehreren Dateien ist. Das ist eine gute Abstraktion.


2. Offizielle API des Pakets definieren (__all__)



from my_package import * (Wildcard-Import) ist praktisch, kann aber dazu führen, dass unerwünschte Variablen oder Module (z.B. import os) in den aktuellen Namensraum importiert werden und diesen verschmutzen.

__all__ definiert eine „Whitelist“, die besagt, „nur die Elemente in dieser Liste importieren“, wenn import * verwendet wird.

# payments/views/__init__.py

# Module für den internen Gebrauch
import logging 
from .internal_utils import _helper_function

# Views, die nach außen sichtbar sind
from .user_views import UserProfileView
from .payment_views import PaymentView

# __all__ Definition:
# "Wenn import * verwendet wird, können nur UserProfileView und PaymentView importiert werden."
__all__ = [
    'UserProfileView',
    'PaymentView'
]

Jetzt werden beim Ausführen von from payments.views import * das logging-Modul oder die interne Funktion _helper_function nicht importiert, sodass der Namensraum sicher geschützt bleibt.


3. Initialisierung des Pakets und Definition von Metadaten

Dies ist der perfekte Ort, um Code, der nur einmal beim ersten Laden des Pakets ausgeführt werden soll, unterzubringen.

  • Versionsinformationen bereitstellen:
# my_library/__init__.py
__version__ = "1.2.0"
__author__ = "whitedec"

Jetzt können Benutzer leicht die Version mit import my_library; print(my_library.__version__) überprüfen.

  • Globale Einstellungen (Setup) ausführen:

Ein typisches Beispiel ist, wenn Django import django verwendet und intern django.setup() aufruft, oder wenn der Code zur Initialisierung von Celery im init.py enthalten ist.


# my_project/__init__.py (Celery Beispiel)

# Gewährleistet, dass die Celery-App zusammen mit Django geladen wird
from .celery import app as celery_app

__all__ = ('celery_app',)


※ Hinweis: Zirkuläre Verweise zwischen internen Modulen

Wenn man __init__.py als 'Leitstelle' verwendet, gibt es Dinge zu beachten.

Angenommen, views/webhooks.py muss CancelView von views/payment_commons.py verwenden.

  • Schlechte Methode (Risiko zirkulärer Verweise):

Versuch, in webhooks.py über from . import CancelView (also über init.py) darauf zuzugreifen.

  • Gute Methode (direkter Import):

Direkter Zugriff auf die notwendigen Module in webhooks.py durch from .payment_commons import CancelView.

Regeln:

  • Externe Benutzer (z.B. urls.py): verwenden die API über die 'Leitstelle' (__init__.py).
  • Interne Module (z.B. webhooks.py): greifen direkt auf Funktionen von Kollegenmodulen (payment_commons.py) zu, ohne die 'Leitstelle' zu nutzen.

Fazit

Die Datei __init__.py ist nicht einfach eine leere Datei, sondern ein leistungsstarkes Werkzeug, das die Rollen einer 'Leitstelle', 'offiziellen API-Dokumentation' und 'Initialisierungsskript' übernehmen kann. Durch die richtige Nutzung dieser Datei kann die Struktur eines Projekts weitaus klarer und wartungsfreundlicher gestaltet werden.