Beide Varianten haben ihre Verwendung. In den meisten Fällen ist es jedoch besser, außerhalb der Funktionen zu importieren, nicht innerhalb dieser.
Performance
Es wurde in mehreren Antworten erwähnt, aber meiner Meinung nach fehlt allen eine vollständige Diskussion.
Wenn ein Modul zum ersten Mal in einen Python-Interpreter importiert wird, ist es langsam, unabhängig davon, ob es sich in der obersten Ebene oder in einer Funktion befindet. Es ist langsam, weil Python (ich konzentriere mich auf CPython, es könnte für andere Python-Implementierungen anders sein) mehrere Schritte ausführt:
- Findet das Paket.
- Überprüft, ob das Paket bereits in Bytecode (das berühmte
__pycache__
Verzeichnis oder die .pyx
Dateien) konvertiert wurde, und wenn nicht, konvertiert es diese in Bytecode.
- Python lädt den Bytecode.
- Das geladene Modul wird eingelegt
sys.modules
.
Nachfolgende Importe müssen nicht alle diese Aufgaben ausführen, da Python das Modul einfach von zurückgeben kann sys.modules
. So werden nachfolgende Importe viel schneller sein.
Es kann sein, dass eine Funktion in Ihrem Modul nicht sehr oft verwendet wird, dies hängt jedoch von einer Funktion ab import
, die ziemlich lange dauert. Dann könnten Sie tatsächlich das import
Innere der Funktion verschieben. Dies beschleunigt den Import Ihres Moduls (da das lange Ladepaket nicht sofort importiert werden muss). Wenn die Funktion jedoch endgültig verwendet wird, ist sie beim ersten Aufruf langsam (da das Modul dann importiert werden muss). Dies kann sich auf die wahrgenommene Leistung auswirken, da Sie nicht alle Benutzer verlangsamen, sondern nur diejenigen, die die Funktion verwenden, die von der Abhängigkeit vom langsamen Laden abhängt.
Die Suche in sys.modules
ist jedoch nicht kostenlos. Es ist sehr schnell, aber nicht kostenlos. Wenn Sie also tatsächlich eine Funktion aufrufen, die ein import
Paket ist, werden Sie eine leicht verschlechterte Leistung feststellen:
import random
import itertools
def func_1():
return random.random()
def func_2():
import random
return random.random()
def loopy(func, repeats):
for _ in itertools.repeat(None, repeats):
func()
%timeit loopy(func_1, 10000)
%timeit loopy(func_2, 10000)
Das ist fast zweimal langsamer.
Es ist sehr wichtig zu erkennen, dass aaronasterling in der Antwort ein bisschen "betrogen" hat . Er erklärte, dass der Import in die Funktion die Funktion tatsächlich schneller macht. Und bis zu einem gewissen Grad ist dies wahr. Das liegt daran, wie Python nach Namen sucht:
- Es überprüft zuerst den lokalen Bereich.
- Als nächstes wird der umgebende Bereich überprüft.
- Dann wird der nächste umgebende Bereich überprüft
- ...
- Der globale Bereich wird überprüft.
Anstatt den lokalen Bereich und dann den globalen Bereich zu überprüfen, reicht es aus, den lokalen Bereich zu überprüfen, da der Name des Moduls im lokalen Bereich verfügbar ist. Das macht es tatsächlich schneller! Aber das ist eine Technik namens "Schleifeninvariante Codebewegung" . Dies bedeutet im Grunde, dass Sie den Overhead von etwas reduzieren, das in einer Schleife (oder wiederholt) ausgeführt wird, indem Sie es in einer Variablen vor der Schleife (oder den wiederholten Aufrufen) speichern. Anstatt import
es in der Funktion zu verwenden, können Sie auch einfach eine Variable verwenden und sie dem globalen Namen zuweisen:
import random
import itertools
def f1(repeats):
"Repeated global lookup"
for _ in itertools.repeat(None, repeats):
random.random()
def f2(repeats):
"Import once then repeated local lookup"
import random
for _ in itertools.repeat(None, repeats):
random.random()
def f3(repeats):
"Assign once then repeated local lookup"
local_random = random
for _ in itertools.repeat(None, repeats):
local_random.random()
%timeit f1(10000)
%timeit f2(10000)
%timeit f3(10000)
Während Sie deutlich sehen können, dass wiederholte Suchvorgänge für das globale random
Modul langsam sind, gibt es praktisch keinen Unterschied zwischen dem Importieren des Moduls in die Funktion oder dem Zuweisen des globalen Moduls in einer Variablen innerhalb der Funktion.
Dies könnte bis zum Äußersten gehen, indem auch die Funktionssuche innerhalb der Schleife vermieden wird:
def f4(repeats):
from random import random
for _ in itertools.repeat(None, repeats):
random()
def f5(repeats):
r = random.random
for _ in itertools.repeat(None, repeats):
r()
%timeit f4(10000)
%timeit f5(10000)
Wieder viel schneller, aber es gibt fast keinen Unterschied zwischen dem Import und der Variablen.
Optionale Abhängigkeiten
Manchmal kann ein Import auf Modulebene tatsächlich ein Problem sein. Wenn Sie beispielsweise keine weitere Abhängigkeit von der Installationszeit hinzufügen möchten, das Modul jedoch für einige zusätzliche Funktionen sehr hilfreich ist. Die Entscheidung, ob eine Abhängigkeit optional sein soll, sollte nicht leichtfertig getroffen werden, da dies die Benutzer betrifft (entweder wenn sie unerwartet sind ImportError
oder auf andere Weise die "coolen Funktionen" verpassen) und die Installation des Pakets mit allen Funktionen für den Normalzustand komplizierter wird Abhängigkeiten pip
oderconda
(um nur zwei Paketmanager zu nennen) funktionieren sofort, aber für optionale Abhängigkeiten müssen die Benutzer Pakete später manuell installieren (es gibt einige Optionen, die es ermöglichen, die Anforderungen anzupassen, aber dann wieder die Last der Installation). richtig "wird auf den Benutzer gelegt).
Aber auch dies könnte auf beide Arten geschehen:
try:
import matplotlib.pyplot as plt
except ImportError:
pass
def function_that_requires_matplotlib():
plt.plot()
oder:
def function_that_requires_matplotlib():
import matplotlib.pyplot as plt
plt.plot()
Dies könnte angepasst werden, indem alternative Implementierungen bereitgestellt oder die Ausnahme (oder Nachricht) angepasst werden, die der Benutzer sieht. Dies ist jedoch der Hauptinhalt.
Der Top-Level-Ansatz könnte etwas besser sein, wenn man eine alternative "Lösung" für die optionale Abhängigkeit bereitstellen möchte, jedoch wird im Allgemeinen der In-Function-Import verwendet. Meistens, weil es zu einer saubereren Stapelspur führt und kürzer ist.
Zirkuläre Importe
In-Function-Importe können sehr hilfreich sein, um ImportErrors aufgrund von zirkulären Importen zu vermeiden. In vielen Fällen sind zirkuläre Importe ein Zeichen für eine "schlechte" Paketstruktur, aber wenn es absolut keine Möglichkeit gibt, einen zirkulären Import zu vermeiden, werden die "Kreise" (und damit die Probleme) gelöst, indem die Importe, die zum Kreis führen, hineingelegt werden die Funktionen, die es tatsächlich verwenden.
Wiederhole dich nicht
Wenn Sie tatsächlich alle Importe in die Funktion anstelle des Modulbereichs einfügen, wird Redundanz eingeführt, da Funktionen wahrscheinlich dieselben Importe erfordern. Das hat ein paar Nachteile:
- Sie haben jetzt mehrere Stellen, an denen Sie überprüfen können, ob ein Import veraltet ist.
- Falls Sie einen Import falsch geschrieben haben, werden Sie dies erst herausfinden, wenn Sie die spezifische Funktion ausführen und nicht beim Laden. Da Sie mehr Importanweisungen haben, steigt die Wahrscheinlichkeit eines Fehlers (nicht viel) und es wird nur ein kleines bisschen wichtiger, alle Funktionen zu testen.
Zusätzliche Gedanken:
Am Ende meiner Module befindet sich selten eine Litanei von Importen, von denen die Hälfte oder mehr ich nicht mehr benötige, weil ich sie überarbeitet habe.
Die meisten IDEs verfügen bereits über einen Prüfer für nicht verwendete Importe, sodass dies wahrscheinlich nur ein paar Klicks sind, um sie zu entfernen. Selbst wenn Sie keine IDE verwenden, können Sie gelegentlich ein statisches Code-Überprüfungsskript verwenden und es manuell reparieren. Eine andere Antwort erwähnte Pylint, aber es gibt andere (zum Beispiel Pyflakes).
Ich verschmutzte meine Module selten versehentlich mit dem Inhalt anderer Module
Aus diesem Grund verwenden __all__
und / oder definieren Sie normalerweise Ihre Funktionssubmodule und importieren nur die relevanten Klassen / Funktionen / ... in das Hauptmodul, z __init__.py
.
Auch wenn Sie der Meinung sind, dass Sie den Modul-Namespace zu stark verschmutzt haben, sollten Sie wahrscheinlich in Betracht ziehen, das Modul in Submodule aufzuteilen. Dies ist jedoch nur für Dutzende von Importen sinnvoll.
Ein zusätzlicher (sehr wichtiger) Punkt, den Sie erwähnen sollten, wenn Sie die Verschmutzung durch Namespaces reduzieren möchten, ist die Vermeidung von from module import *
Importen. Möglicherweise möchten Sie aber auch from module import a, b, c, d, e, ...
Importe vermeiden , die zu viele Namen importieren, und einfach das Modul importieren und mit auf die Funktionen zugreifen module.c
.
Als letzten Ausweg können Sie immer Aliase verwenden, um zu vermeiden, dass der Namespace mit "öffentlichen" Importen verschmutzt wird, indem Sie Folgendes verwenden : import random as _random
. Dadurch wird der Code schwerer zu verstehen, aber es wird sehr deutlich, was öffentlich sichtbar sein sollte und was nicht. Ich würde es nicht empfehlen, Sie sollten nur die __all__
Liste auf dem neuesten Stand halten (was der empfohlene und vernünftige Ansatz ist).
Zusammenfassung
Die Auswirkungen auf die Leistung sind sichtbar, werden jedoch fast immer mikrooptimiert. Lassen Sie sich also nicht von Mikro-Benchmarks leiten, wo Sie die Importe platzieren. Außer wenn die Abhängigkeit zuerst sehr langsam ist import
und nur für einen kleinen Teil der Funktionalität verwendet wird. Dann kann es für die meisten Benutzer tatsächlich einen sichtbaren Einfluss auf die wahrgenommene Leistung Ihres Moduls haben.
Verwenden Sie die allgemein verständlichen Tools zum Definieren der öffentlichen API, ich meine die __all__
Variable. Es mag etwas ärgerlich sein, es auf dem neuesten Stand zu halten, aber es überprüft auch alle Funktionen auf veraltete Importe oder wenn Sie eine neue Funktion hinzufügen, um alle relevanten Importe in dieser Funktion hinzuzufügen. Auf lange Sicht müssen Sie wahrscheinlich weniger Arbeit durch Aktualisierung erledigen __all__
.
Es ist wirklich egal, welches Sie bevorzugen, beide arbeiten. Wenn Sie alleine arbeiten, können Sie über die Vor- und Nachteile nachdenken und das tun, was Sie für das Beste halten. Wenn Sie jedoch in einem Team arbeiten, sollten Sie sich wahrscheinlich an bekannte Muster halten (bei denen es sich um Importe auf höchster Ebene handelt __all__
), da sie damit das tun können, was sie (wahrscheinlich) immer getan haben.