web-dev-qa-db-ja.com

Pythonコーディングスタイルのインポート

新しいパターンを発見しました。このパターンはよく知られていますか、それともどのような意見ですか?

基本的に、ソースファイルを上下にスクラブして、どのモジュールのインポートが利用可能であるかなどを把握するのに苦労しています。今では、

import foo
from bar.baz import quux

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

次のように、すべてのインポートを実際に使用される関数に移動します。

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

    foo.this.that(quux)

これはいくつかのことを行います。まず、他のモジュールの内容でモジュールを誤って汚染することはめったにありません。 __all__モジュールの変数ですが、モジュールの進化に合わせて更新する必要があります。これは、モジュールに実際に存在するコードの名前空間の汚染には役立ちません。

第二に、モジュールの一番上に大量のインポートが存在することはめったにありません。その半分以上は、リファクタリングしたために不要になりました。最後に、すべての参照された名前が関数本体内にあるため、このパターンは非常に読みやすくなっています。

62
TokenMacGuy

この質問の(以前の) トップ投票の回答 はうまくフォーマットされていますが、パフォーマンスに関しては完全に間違っています。実演させてください

パフォーマンス

トップインポート

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

関数本体にインポート

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

ご覧のとおり、モジュールを関数にインポートする方がmore効率的です。この理由は簡単です。参照をグローバル参照からローカル参照に移動します。つまり、少なくともCPythonの場合、コンパイラーはLOAD_FAST命令ではなくLOAD_GLOBAL命令を発行します。名前が示すように、これらはより高速です。他の回答者は、ループの1回の反復ごとにimportによってsys.modulesを調べるパフォーマンスヒットを作為的に増大させました

原則として、最初からインポートするのが最善ですが、モジュールに何度もアクセスする場合は、パフォーマンスがではないのが理由です。その理由は、モジュールが依存するものをより簡単に追跡できることと、そうすることは、Pythonユニバースの残りのほとんどと一貫していることです。

108
aaronasterling

これにはいくつかの欠点があります。

テスト中

実行時の変更を通じてモジュールをテストしたい場合は、さらに難しくなる可能性があります。する代わりに

import mymodule
mymodule.othermodule = module_stub

あなたがしなければならないでしょう

import othermodule
othermodule.foo = foo_stub

つまり、mymoduleの参照が指すものを変更するだけではなく、othermoduleにグローバルにパッチを適用する必要があります。

依存関係の追跡

これにより、モジュールがどのモジュールに依存しているかがわかりにくくなります。これは、多くのサードパーティライブラリを使用している場合や、コードを再編成している場合に特に不快です。

どこでもインラインでインポートを使用するいくつかのレガシーコードを維持する必要があり、コードのリファクタリングやリパッケージが非常に困難になりました。

パフォーマンスに関する注意

pythonがモジュールをキャッシュする方法のため、パフォーマンスに影響はありません。実際、モジュールはローカルの名前空間にあるため、関数にモジュールをインポートする方がパフォーマンスにわずかな利点があります。

トップインポート

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

関数本体にインポート

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

このアプローチのいくつかの問題:

  • 依存するモジュールをファイルで開いたときに、すぐにはわかりません。
  • py2exepy2appなどの依存関係を分析する必要があるプログラムを混乱させます。
  • 多くの機能で使用するモジュールはどうですか?冗長なインポートが大量に発生するか、ファイルの先頭にいくつかを配置し、内部関数をいくつか配置する必要があります。

したがって...推奨される方法は、すべてのインポートをファイルの先頭に配置することです。インポートの追跡が困難になる場合、通常はコードが多すぎて2つ以上のファイルに分割した方がよいことを発見しました。

私がhaveであるいくつかの状況では、関数内のインポートが有用であることがわかりました。

  • 循環依存関係に対処するため(本当に回避できない場合)
  • プラットフォーム固有のコード

また、各関数内にインポートを配置すると、実際にはnotがファイルの先頭よりもかなり遅くなります。各モジュールが初めてロードされるとき、それはsys.modulesに入れられ、その後の各インポートには、モジュールを検索する時間だけがかかります。これはかなり高速です(再ロードされません)。

21
dF.

注意すべきもう1つの便利な点は、from module import *関数内の構文は、Python 3.0で削除されました。

ここの「削除された構文」の下にそれについての簡単な言及があります:

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

10
Russell Bryant

from foo import barのインポートを回避することをお勧めします。私はそれらをパッケージ内でのみ使用します。モジュールへの分割は実装の詳細であり、いずれにせよそれらの多くはありません。

パッケージをインポートする他のすべての場所では、import fooを使用し、フルネームfoo.barで参照します。このようにして、特定の要素がどこから来ているかを常に知ることができ、インポートされた要素のリストを維持する必要はありません(実際、これは常に古くなり、使用されなくなった要素をインポートします)。

fooが本当に長い名前である場合は、import foo as fで簡略化してから、f.barと記述できます。これは、すべてのfromインポートを維持するよりもはるかに便利で明示的です。

4
nikow

インラインインポートを避ける理由はよく説明されていますが、最初に必要な理由に対処するための代替ワークフローではありません。

ソースファイルを上下にスクラブして、どのモジュールのインポートが利用可能かなどを把握するのに苦労しています

未使用のインポートを確認するには、 pylint を使用します。これは、Pythonコードの静的(ish)分析を行い、チェックする(多くの)ことの1つは未使用のインポートです。たとえば、次のスクリプトです。

import urllib
import urllib2

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

..次のメッセージを生成します:

example.py:2 [W0611] Unused import urllib2

利用可能なインポートのチェックに関しては、私は通常TextMateの(かなり単純化された)補完に依存しています。Escを押すと、現在のWordがドキュメント内の他の単語と共に補完されます。私がやった場合import urlliburll[Esc]urllibに展開されます。そうでない場合は、ファイルの先頭にジャンプしてインポートを追加します。

3
dbr

python wikiのImport ステートメントオーバーヘッド を見てください。つまり、モジュールが既にロードされている場合(sys.modules)コードの実行速度が遅くなります。モジュールがまだ読み込まれておらず、fooが必要なときにのみ読み込まれる場合(これは0回になる場合があります)、全体的なパフォーマンスが向上します。

2
RSabet

パフォーマンスの観点からは、これを見ることができます: Python importステートメントは常にモジュールの先頭にあるべきですか?

一般的に、依存関係の循環を解消するために、ローカルインポートのみを使用します。

2
sykora

どちらのバリアントにも用途があります。ただし、ほとんどの場合、関数の内部ではなく、関数の外部にインポートすることをお勧めします。

パフォーマンス

それはいくつかの回答で言及されていますが、私の意見では、それらはすべて完全な議論に欠けています。

モジュールがpythonインタープリターに初めてインポートされるときは、トップレベルにあるか、関数内にあるかに関係なく、低速になります。 Python(私はCPythonに焦点を合わせています。他のPythonの実装では異なる場合があります)が複数のステップを実行するため、処理が遅くなります。

  • パッケージを見つけます。
  • パッケージがすでにバイトコード(有名な__pycache__ディレクトリまたは.pyxファイル)に変換されているかどうかをチェックし、そうでない場合はバイトコードに変換します。
  • Pythonがバイトコードをロードします。
  • ロードされたモジュールは sys.modules に置かれます。

Pythonは単純に sys.modules からモジュールを返すことができるため、後続のインポートでこれらすべてを実行する必要はありません。したがって、その後のインポートははるかに速くなります。

モジュール内の関数が実際にはあまり使用されない可能性がありますが、かなり時間がかかっているimportに依存します。次に、実際にimportを関数内に移動できます。これにより、モジュールのインポートが速くなります(長時間ロードパッケージをすぐにインポートする必要がないため)。ただし、関数が最後に使用されると、最初の呼び出しが遅くなります(モジュールをインポートする必要があるため)。すべてのユーザーを遅くするのではなく、低速ロードの依存関係に依存する関数を使用するユーザーのみを遅くするため、これは知覚されるパフォーマンスに影響を与える可能性があります。

ただし、sys.modulesの検索は無料ではありません。非常に高速ですが、無料ではありません。したがって、実際にパッケージをimportsする関数を頻繁に呼び出すと、パフォーマンスがわずかに低下します。

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)

それはほぼ2倍遅いです。

aaronasterlingが回答で少し「だまされた」 であることを認識することは非常に重要です。関数でインポートを行うと、実際には関数が高速になると彼は述べた。そして、ある程度、これは真実です。これは、Pythonが名前を検索する方法が原因です。

  • 最初にローカルスコープをチェックします。
  • 次に、周囲のスコープをチェックします。
  • 次に、次の周囲のスコープがチェックされます
  • ...
  • グローバルスコープがチェックされます。

したがって、ローカルスコープをチェックしてからグローバルスコープをチェックする代わりに、モジュールの名前がローカルスコープで使用できるため、ローカルスコープをチェックするだけで十分です。それは実際にはそれをより速くします!しかし、それは "Loop-invariant code motion" と呼ばれる手法です。これは基本的に、ループ(または繰り返し呼び出し)の前に変数に格納することにより、ループ(または繰り返し)で行われる処理のオーバーヘッドを削減することを意味します。したがって、関数でimportingする代わりに、変数を使用してグローバル名に割り当てることもできます。

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)

グローバルrandomの繰り返しルックアップを実行すると時間がかかることがはっきりとわかりますが、関数内のモジュールのインポートと関数内の変数のグローバルモジュールの割り当てに実質的に違いはありません。

これは、ループ内の関数ルックアップも回避することにより、極端にすることができます。

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)

繰り返しになりますが、インポートと変数の違いはほとんどありません。

オプションの依存関係

モジュールレベルのインポートが実際に問題になる場合があります。たとえば、別のインストール時の依存関係を追加したくないが、モジュールが一部の追加機能に本当に役立つ場合などです。依存関係をオプションにするかどうかの決定は、ユーザーに影響を及ぼし(予期しないImportErrorを取得するか、「クールな機能」を逃した場合)、すべてのパッケージをインストールするため、軽く行うべきではありません。機能はより複雑で、通常の依存関係の場合はpipまたはconda(2つのパッケージマネージャーについて言及)はそのまま使用できますが、オプションの依存関係の場合は、ユーザーがパッケージを後で手動でインストールする必要があります(要件をカスタマイズすることを可能にするいくつかのオプションがありますが、再び「正しく」インストールする負担がユーザーにかかります)。

しかし、これは両方の方法で行うことができます:

try:
    import matplotlib.pyplot as plt
except ImportError:
    pass

def function_that_requires_matplotlib():
    plt.plot()

または:

def function_that_requires_matplotlib():
    import matplotlib.pyplot as plt
    plt.plot()

これは、代替実装を提供するか、ユーザーに表示される例外(またはメッセージ)をカスタマイズすることで、よりカスタマイズできますが、これが主な要点です。

オプションの依存関係に対する代替の「ソリューション」を提供したい場合は、トップレベルのアプローチが少し良いかもしれませんが、一般的に人々は関数内インポートを使用します。主にそれはよりきれいなスタックトレースにつながり、より短いからです。

循環輸入

関数内インポートは、循環インポートによるImportErrorsを回避するのに非常に役立ちます。多くの場合、循環インポートは「悪い」パッケージ構造の兆候ですが、循環インポートを回避する方法がまったくない場合は、循環につながるインポートを配置することで「循環」(したがって問題)を解決します実際にそれを使用する関数。

自分を繰り返さないでください

すべてのインポートをモジュールスコープの代わりに関数に実際に配置すると、関数が同じインポートを必要とする可能性が高いため、冗長性が生じます。これにはいくつかの欠点があります:

  • インポートが廃止されているかどうかを確認する場所が複数あります。
  • 一部のインポートのスペルを間違えた場合は、特定の関数を実行したときだけで、読み込み時ではないことがわかります。インポート文が多いため、間違いの可能性が高まり(それほどではありません)、すべての関数をテストすることが非常に重要になります。

追加の考え:

モジュールの一番上で大量のインポートが発生することはめったにありません。その半分以上は、リファクタリングしたので不要になりました。

ほとんどのIDEには、未使用のインポートのチェッカーがすでにあるので、数回クリックするだけでそれらを削除できます。 IDEを使用しない場合でも、静的コードチェッカースクリプトを使用して、手動で修正することができます。別の回答では、パイリントについて言及しましたが、他にもあります(たとえば、パイフレーク)。

モジュールを他のモジュールの内容で誤って汚染することはめったにありません

そのため、通常、__all__を使用したり、関数のサブモジュールを定義したり、メインのモジュールに関連するクラス/関数/ ...だけをインポートしたりします(__init__.pyなど)。

また、モジュールの名前空間を過度に汚染していると思われる場合は、モジュールをサブモジュールに分割することを検討する必要がありますが、これは数十のインポートに対してのみ意味があります。

名前空間の汚染を減らしたい場合に言及すべきもう1つの(非常に重要な)ポイントは、from module import *インポートを回避することです。ただし、多すぎる名前をインポートするfrom module import a, b, c, d, e, ...インポートを避け、モジュールをインポートしてmodule.cで関数にアクセスすることもできます。

最後の手段として、常にエイリアスを使用して、「import random as _random」を使用することにより、「パブリック」インポートでネームスペースを汚染することを回避できます。これはコードを理解するのを難しくしますが、それは何が公に見えるべきか、何がそうでないべきかを非常に明確にします。これは私がお勧めするものではありません。__all__リストを最新の状態に保つ必要があります(推奨される賢明な方法です)。

概要

  • パフォーマンスへの影響は目に見えますが、ほとんどの場合、それはマイクロ最適化です。そのため、インポートを配置する場所の決定がマイクロベンチマークによって導かれないようにしてください。依存関係が最初のimportで本当に遅い場合を除いて、機能の小さなサブセットにのみ使用されます。そうなると、ほとんどのユーザーにとって、モジュールの知覚パフォーマンスに目に見える影響を与える可能性があります。

  • 一般に理解されているツールを使用してパブリックAPIを定義します。つまり、__all__変数です。最新の状態に保つのは少し面倒かもしれませんが、すべての関数で古いインポートをチェックしている場合や、新しい関数を追加してその関数に関連するすべてのインポートを追加している場合です。長期的には、__all__を更新することで、作業量を減らす必要があります。

  • どちらを選択してもかまいません。どちらも機能します。一人で作業している場合は、長所と短所について推論し、どれが一番良いと思うかを行うことができます。ただし、チームで作業している場合は、既知のパターン(__all__を使用した最上位のインポート)に固執する必要があります。これにより、(おそらく)常に行っていることを実行できるようになります。

2
MSeifert

これはいくつかのケース/シナリオで推奨されるアプローチだと思います。たとえば、Google App Engineでは、大きなモジュールの遅延読み込みをお勧めします。これにより、新しいPython VMs/interpreterをインスタンス化する際のウォームアップコストが最小限に抑えられます。 Google Engineer's これを説明するプレゼンテーションですが、これはしないことを意味するので、すべてのモジュールを遅延ロードする必要があります。

2
fuentesjr