it-swarm.com.de

Python-Import-Codierungsstil

Ich habe ein neues Muster entdeckt. Ist dieses Muster bekannt oder wie ist die Meinung dazu?

Grundsätzlich fällt es mir schwer, Quelldateien nach oben und unten zu scrubben, um herauszufinden, welche Modulimporte verfügbar sind und so weiter, also jetzt statt

import foo
from bar.baz import quux

def myFunction():
    foo.this.that(quux)

Ich verschiebe alle meine Importe in die Funktion, in der sie tatsächlich verwendet werden.

def myFunction():
    import foo
    from bar.baz import quux

    foo.this.that(quux)

Das macht ein paar Dinge. Erstens verseuche ich meine Module selten versehentlich mit dem Inhalt anderer Module. Ich könnte die Variable __all__ für das Modul festlegen, aber dann müsste ich sie aktualisieren, wenn sich das Modul weiterentwickelt, und dies trägt nicht zur Verschmutzung des Namespaces für Code bei, der tatsächlich im Modul enthalten ist.

Zweitens ende ich selten mit einer Fülle von Importen an der Spitze meiner Module, von denen ich die Hälfte oder mehr nicht mehr benötige, weil ich sie überarbeitet habe. Schließlich finde ich dieses Muster VIEL einfacher zu lesen, da jeder referenzierte Name genau im Funktionskörper vorhanden ist.

59
TokenMacGuy

Die (zuvor) am häufigsten gewählte Antwort auf diese Frage ist schön formatiert, aber absolut falsch in Bezug auf die Leistung. Lass mich demonstrieren

Performance

Top Import

import random

def f():
    L = []
    for i in xrange(1000):
        L.append(random.random())


for i in xrange(1000):
    f()

$ time python import.py

real        0m0.721s
user        0m0.412s
sys         0m0.020s

In Funktionskörper importieren

def f():
    import random
    L = []
    for i in xrange(1000):
        L.append(random.random())

for i in xrange(1000):
    f()

$ time python import2.py

real        0m0.661s
user        0m0.404s
sys         0m0.008s

Wie Sie sehen, kann es mehr effizient sein, das Modul in die Funktion zu importieren. Der Grund dafür ist einfach. Der Verweis wird von einem globalen Verweis auf einen lokalen Verweis verschoben. Dies bedeutet, dass der Compiler zumindest für CPython LOAD_FAST -Anweisungen anstelle von LOAD_GLOBAL -Anweisungen ausgibt. Diese sind, wie der Name schon sagt, schneller. Der andere Anrufbeantworter hat den Performance-Hit des Schauens in sys.modules künstlich erhöht, indem er bei jeder einzelnen Iteration der Schleife importiert .

In der Regel ist es am besten, oben zu importieren, aber die Leistung ist nicht der Grund, wenn Sie häufig auf das Modul zugreifen. Die Gründe sind, dass man leichter verfolgen kann, wovon ein Modul abhängt, und dass dies mit den meisten anderen Bereichen des Python -Universums vereinbar ist.

106
aaronasterling

Dies hat einige Nachteile.

Testen

Wenn Sie Ihr Modul während der Laufzeitänderung testen möchten, kann dies die Ausführung erschweren. Anstatt zu tun

import mymodule
mymodule.othermodule = module_stub

Du musst es tun

import othermodule
othermodule.foo = foo_stub

Das bedeutet, dass Sie das andere Modul global patchen müssen, statt nur das zu ändern, worauf die Referenz in mymodule verweist.

Abhängigkeitsverfolgung

Daher ist es nicht offensichtlich, von welchen Modulen Ihr Modul abhängt. Dies ist besonders irritierend, wenn Sie viele Bibliotheken von Drittanbietern verwenden oder Code neu organisieren.

Ich musste einen alten Code beibehalten, bei dem Importe überall eingesetzt wurden. Dies machte es sehr schwierig, den Code zu refactorieren oder neu zu verpacken.

Hinweise zur Leistung

Aufgrund der Art und Weise, in der Python-Module zwischengespeichert werden, gibt es keinen Leistungseinbruch. Da sich das Modul im lokalen Namespace befindet, ergibt sich ein geringfügiger Leistungsvorteil beim Importieren von Modulen in einer Funktion.

Top-Import

import random

def f():
    L = []
    for i in xrange(1000):
        L.append(random.random())

for i in xrange(10000):
    f()


$ time python test.py 

real   0m1.569s
user   0m1.560s
sys    0m0.010s

In Funktionskörper importieren

def f():
    import random
    L = []
    for i in xrange(1000):
        L.append(random.random())

for i in xrange(10000):
    f()

$ time python test2.py

real    0m1.385s
user    0m1.380s
sys     0m0.000s
53
Ryan

Einige Probleme mit diesem Ansatz:

  • Beim Öffnen der Datei ist nicht sofort ersichtlich, von welchen Modulen sie abhängt.
  • Dies wird Programme verwirren, die Abhängigkeiten analysieren müssen, wie py2exe, py2app usw.
  • Was ist mit Modulen, die Sie in vielen Funktionen verwenden? Sie werden entweder mit vielen redundanten Importen enden, oder Sie müssen einige oben in der Datei und einige interne Funktionen haben.

Also ... der bevorzugte Weg ist, alle Importe an den Anfang der Datei zu setzen. Ich habe festgestellt, dass wenn meine Importe schwer zu verfolgen sind, das bedeutet, dass ich zu viel Code habe, der besser in zwei oder mehr Dateien aufgeteilt werden könnte.

Einige Situationen, in denen ich habe importierte Funktionen innerhalb von Funktionen als nützlich erachtet:

  • Umgehen mit zirkularen Abhängigkeiten (wenn Sie sie wirklich nicht vermeiden können)
  • Plattformspezifischer Code

Außerdem: Das Einfügen von Importen in jede Funktion ist tatsächlich nicht merklich langsamer als am Anfang der Datei. Beim erstmaligen Laden jedes Moduls wird es in sys.modules abgelegt, und jeder nachfolgende Import kostet nur die Zeit, um das Modul nachzuschlagen, was ziemlich schnell ist (es wird nicht neu geladen).

19
dF.

Eine weitere nützliche Sache ist, dass die from module import *-Syntax innerhalb einer Funktion in Python 3.0 entfernt wurde.

Es gibt hier eine kurze Erwähnung unter "Entfernte Syntax":

http://docs.python.org/3.0/whatsnew/3.0.html

10
Russell Bryant

Ich würde vorschlagen, dass Sie versuchen, from foo import bar-Importe zu vermeiden. Ich verwende sie nur innerhalb von Paketen, bei denen die Aufteilung in Module ein Implementierungsdetail ist, von denen es ohnehin nicht viele gibt.

An allen anderen Stellen, an denen Sie ein Paket importieren, verwenden Sie einfach import foo und referenzieren Sie es dann mit dem vollständigen Namen foo.bar. Auf diese Weise können Sie immer erkennen, woher ein bestimmtes Element stammt, und müssen nicht die Liste der importierten Elemente verwalten (in der Realität ist dies immer veraltet und importieren Sie nicht mehr verwendete Elemente). 

Wenn foo ein wirklich langer Name ist, können Sie ihn mit import foo as f vereinfachen und dann f.bar schreiben. Dies ist immer noch viel praktischer und expliziter als die Aufrechterhaltung aller from-Importe.

4
nikow

Die Leute haben sehr gut erklärt, warum man Inline-Importe vermeiden sollte, aber nicht wirklich alternative Workflows, um die Gründe zu berücksichtigen, aus denen man sie überhaupt möchte.

Es fällt mir schwer, Quelldateien nach oben und unten zu scrubben, um herauszufinden, welche Modulimporte verfügbar sind und so weiter

Um nicht verwendete Importe zu prüfen, verwende ich pylint . Es führt eine statische (ish) -Analyse von Python-Code durch, und eines der (vielen) Dinge, auf die es prüft, sind ungenutzte Importe. Zum Beispiel das folgende Skript ..

import urllib
import urllib2

urllib.urlopen("http://stackoverflow.com")

..wird folgende Meldung generiert:

example.py:2 [W0611] Unused import urllib2

Bei der Überprüfung der verfügbaren Importe verlasse ich mich im Allgemeinen auf die (relativ einfache) Fertigstellung von TextMate. Wenn Sie die Esc-Taste drücken, wird das aktuelle Word mit den anderen im Dokument vervollständigt. Wenn ich import urllib getan habe, wird urll[Esc] zu urllib erweitert, andernfalls springe ich zum Anfang der Datei und füge den Import hinzu.

3
dbr

Vielleicht möchten Sie einen Blick auf Import Statement Overhead im Python-Wiki werfen. Kurz gesagt: Wenn das Modul bereits geladen wurde (siehe sys.modules), wird der Code langsamer ausgeführt. Wenn Ihr Modul noch nicht geladen wurde und foo nur bei Bedarf geladen wird, was Null sein kann, wird die Gesamtleistung verbessert.

2
RSabet

Aus Performance-Sicht können Sie Folgendes sehen: Sollten sich Python-Importanweisungen immer an der Spitze eines Moduls befinden?

Im Allgemeinen verwende ich nur lokale Importe, um Abhängigkeitszyklen zu unterbrechen.

2
sykora

Ich glaube, dass dies in einigen Fällen/Szenarien ein empfohlener Ansatz ist. In Google App Engine wird zum Beispiel das langsame Laden großer Module empfohlen, da dadurch die Aufwärmkosten für das Instantiieren neuer Python-VMs/Interpreter minimiert werden. Schauen Sie sich eine Google Engineer's -Präsentation an, die dies beschreibt. Denken Sie jedoch daran, dass nicht bedeutet, dass Sie alle Ihre Module faul laden sollten.

2
fuentesjr

Beide Varianten haben ihre Verwendung. In den meisten Fällen ist es jedoch besser, außerhalb der Funktionen zu importieren, nicht innerhalb von Funktionen.

Performance

Es wurde in mehreren Antworten erwähnt, aber meiner Meinung nach fehlt es an einer vollständigen Diskussion.

Wenn ein Modul zum ersten Mal in einen Python-Interpreter importiert wird, ist es langsam, unabhängig davon, ob es sich auf der obersten Ebene oder in einer Funktion befindet. Es ist langsam, weil Python (ich konzentriere mich auf CPython, es könnte sich bei anderen Python-Implementierungen unterscheiden), mehrere Schritte ausführt:

  • Findet das Paket.
  • Überprüft, ob das Paket bereits in Bytecode (das bekannte Verzeichnis __pycache__ oder die Dateien .pyx) konvertiert wurde, und konvertiert sie, falls nicht, in Bytecode.
  • Python lädt den Bytecode.
  • Das geladene Modul wird in sys.modules abgelegt.

Nachfolgende Importe müssen dies nicht alle tun, da Python das Modul einfach von sys.modules zurückgeben kann. Nachfolgende Importe werden also viel schneller sein.

Es kann sein, dass eine Funktion in Ihrem Modul nicht sehr oft verwendet wird, aber es hängt von einer import ab, die ziemlich lange dauert. Dann könnten Sie tatsächlich die import innerhalb der Funktion verschieben. Dadurch wird der Import Ihres Moduls schneller (da das langladende Paket 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 Auswirkungen auf die wahrgenommene Leistung haben, da Sie nicht alle Benutzer verlangsamen, sondern nur diejenigen verlangsamen, die die Funktion verwenden, die von der Abhängigkeit beim langsamen Laden abhängig ist.

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 imports sehr häufig ein Paket ist, werden Sie eine etwas verschlechterte Leistung bemerken:

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)
# 1.14 ms ± 20.6 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit loopy(func_2, 10000)
# 2.21 ms ± 138 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Das ist fast zweimal langsamer.

Es ist sehr wichtig zu wissen, dass aaronasterling in der Antwort ein bisschen "geschummelt" hat . Er erklärte, dass der Import in der Funktion die Funktion tatsächlich beschleunigt. Und bis zu einem gewissen Grad trifft dies zu. Das ist, weil Python Namen nachschlägt:

  • Zuerst wird der lokale Bereich geprüft.
  • Als nächstes wird der Umgebungsumfang geprüft.
  • Dann wird der nächste Umgebungsumfang geprüft
  • ...
  • Der globale Bereich wird geprüft.

Anstatt den lokalen Bereich und dann den globalen Bereich zu prü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, die "Loop-invariante Codebewegung" genannt wird. Das bedeutet im Wesentlichen, 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 importing in der Funktion, 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)
# 588 µs ± 3.92 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit f2(10000)
# 522 µs ± 1.95 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit f3(10000)
# 527 µs ± 4.51 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

Während Sie deutlich erkennen können, dass wiederholte Suchvorgänge für die globale Variable random langsam sind, gibt es praktisch keinen Unterschied zwischen dem Importieren des Moduls innerhalb der Funktion oder dem Zuweisen des globalen Moduls in einer Variablen innerhalb der Funktion.

Dies könnte ins Extreme 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)
# 364 µs ± 9.34 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit f5(10000)
# 357 µs ± 2.73 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

Wieder viel schneller, aber es gibt fast keinen Unterschied zwischen dem Import und der Variablen.

Optionale Abhängigkeiten

Manchmal ist ein Import auf Modulebene tatsächlich ein Problem. Zum Beispiel, wenn Sie keine weitere Installationszeitabhängigkeit hinzufügen möchten, das Modul jedoch für einige Funktionen von additional hilfreich sein könnte. Die Entscheidung, ob eine Abhängigkeit optional sein sollte, sollte nicht leichtfertig getroffen werden, da die Benutzer davon betroffen sind (entweder wenn sie eine unerwartete ImportError erhalten oder die "coolen Funktionen" anderweitig verpassen) und die Installation des Pakets mit allen Funktionen komplizierter wird, z normale Abhängigkeiten pip oder conda (um nur zwei Paketmanager zu erwähnen) funktionieren sofort, aber für optionale Abhängigkeiten müssen die Benutzer Pakete manuell nachinstallieren (es gibt einige Optionen, die es ermöglichen, die Anforderungen anzupassen, aber dann erneut die Last der Installation "richtig" wird dem Benutzer auferlegt).

Dies kann jedoch auch auf zwei Arten erfolgen:

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 kann durch die Bereitstellung alternativer Implementierungen oder das Anpassen der Ausnahme (oder Nachricht), die der Benutzer sieht, angepasst werden. Dies ist jedoch die Hauptübersicht.

Der Ansatz auf oberster Ebene könnte etwas besser sein, wenn eine alternative "Lösung" für die optionale Abhängigkeit bereitgestellt werden soll. In der Regel wird jedoch der In-Funktions-Import verwendet. Meistens weil es zu einem saubereren Stacktrace führt und kürzer ist.

Zirkuläre Importe

In-Function-Importe können sehr hilfreich sein, um ImportErrors aufgrund von Umlaufimporten zu vermeiden. In vielen Fällen sind Zirkularimporte ein Zeichen für eine "schlechte" Paketstruktur, aber wenn es keinen Weg gibt, einen Zirkularimport zu vermeiden, wird der "Kreis" (und damit die Probleme) dadurch gelöst, dass die Importe, die zum Kreis führen, innen gesetzt werden die Funktionen, die es tatsächlich verwenden.

Wiederholen Sie sich nicht

Wenn Sie tatsächlich alle Importe in die Funktion anstelle des Modulumfangs einfügen, führen Sie Redundanz ein, da Funktionen wahrscheinlich die gleichen Importe erfordern. Das hat einige Nachteile:

  • Sie haben jetzt mehrere Stellen, um zu überprüfen, ob ein Import obsolet geworden ist.
  • Falls Sie etwas falsch geschrieben haben, werden Sie nur feststellen, wann Sie die jeweilige Funktion ausführen, und nicht die Ladezeit. Da Sie mehr Anweisungen zum Importieren haben, steigt die Wahrscheinlichkeit eines Fehlers (nicht viel) und es wird nur noch ein kleines bisschen wichtiger, um alle Funktionen zu testen.

Zusätzliche Gedanken:

Ich ende selten mit einer Fülle von Importen an der Spitze meiner Module, von denen ich die Hälfte oder mehr nicht mehr benötige, weil ich sie überarbeitet habe.

Die meisten IDEs verfügen bereits über ein Kontrollkästchen für ungenutzte Importe. Daher sind wahrscheinlich nur wenige Klicks erforderlich, um sie zu entfernen. Selbst wenn Sie kein IDE verwenden, können Sie ab und zu ein Skript für die Überprüfung statischer Codes verwenden und es manuell beheben. Eine andere Antwort erwähnte Pylint, aber es gibt noch andere (zum Beispiel Pyflakes).

Ich versehe meine Module selten versehentlich mit dem Inhalt anderer Module

Deshalb verwenden Sie normalerweise __all__ und/oder definieren Ihre Funktionsmodule und importieren nur die relevanten Klassen/Funktionen/... im Hauptmodul, beispielsweise den __init__.py.

Wenn Sie der Meinung sind, dass Sie den Modulnamensraum zu stark verschmutzt haben, sollten Sie das Modul möglicherweise in Submodule aufteilen. Dies ist jedoch nur für Dutzende von Importen sinnvoll.

Ein weiterer (sehr wichtiger) Punkt, der zu erwähnen ist, wenn Sie die Verschmutzung des Namespaces reduzieren möchten, ist das Vermeiden eines from module import *-Imports. Möglicherweise möchten Sie jedoch auch from module import a, b, c, d, e, ...-Importe vermeiden, die importieren zu viele Namen importieren, und nur das Modul importieren und mit module.c auf die Funktionen zugreifen.

Als letzte Möglichkeit können Sie immer Aliase verwenden, um zu verhindern, dass der Namespace durch "öffentliche" Importe verschmutzt wird, indem Sie Folgendes verwenden: import random as _random. Das macht den Code schwieriger zu verstehen, macht aber deutlich, was öffentlich sichtbar sein sollte und was nicht. Ich würde es nicht empfehlen, Sie sollten die Liste __all__ auf dem neuesten Stand halten (was der empfohlene und vernünftige Ansatz ist).

Zusammenfassung

  • Die Auswirkungen auf die Leistung sind zwar sichtbar, werden jedoch fast immer mikrooptimierend sein. Lassen Sie also nicht die Entscheidung, wo Sie die Importe ablegen, von Mikro-Benchmarks abhängen. Außer, wenn die Abhängigkeit zuerst import wirklich langsam ist und sie nur für einen kleinen Teil der Funktionalität verwendet wird. Dann kann es tatsächlich einen sichtbaren Einfluss auf die wahrgenommene Leistung Ihres Moduls für die meisten Benutzer haben.

  • Verwenden Sie die allgemein bekannten Tools zur Definition der öffentlichen API. Ich meine die Variable __all__. Es ist vielleicht etwas ärgerlich, wenn Sie es auf dem neuesten Stand halten, aber auch alle Funktionen auf obsolete Importe prüfen oder wenn Sie eine neue Funktion hinzufügen, um alle relevanten Importe dieser Funktion hinzuzufügen. Auf lange Sicht müssen Sie wahrscheinlich weniger Arbeit erledigen, indem Sie __all__ aktualisieren.

  • Es ist wirklich egal, welchen 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 (was Importe auf oberster Ebene mit __all__ wäre), da sie es ihnen ermöglichen, das zu tun, was sie (wahrscheinlich) immer getan haben.

0
MSeifert