import ステートメントは常にモジュールの先頭に置く必要がありますか?
-
02-07-2019 - |
質問
PEP 08 状態:
インポートは常にファイルの先頭、モジュールのコメントとドキュメント文字列の直後、モジュールのグローバルと定数の前に配置されます。
しかし、インポートしているクラス/メソッド/関数がまれなケースでしか使用されない場合、必要なときにインポートを実行する方が効率的ではないでしょうか?
これではないでしょうか:
class SomeClass(object):
def not_often_called(self)
from datetime import datetime
self.datetime = datetime.now()
これより効率的ですか?
from datetime import datetime
class SomeClass(object):
def not_often_called(self)
self.datetime = datetime.now()
解決
モジュールのインポートは非常に高速ですが、即時ではありません。この意味は:
- import をモジュールの先頭に置くのは問題ありません。これは、一度だけ支払われる些細なコストだからです。
- インポートを関数内に置くと、その関数の呼び出しに時間がかかります。
したがって、効率を重視する場合は、輸入を最上位に置きます。プロファイリングでそれが役立つことが示された場合にのみ、それらを関数に移動します ( した パフォーマンスを向上させるのに最適な場所を確認するには、プロファイルを参照してください??)
遅延インポートを実行する主な理由は次のとおりです。
- オプションのライブラリのサポート。コードに異なるライブラリを使用する複数のパスがある場合、オプションのライブラリがインストールされていない場合でも中断しないでください。
- の中に
__init__.py
インポートされている可能性がありますが、実際には使用されていない可能性があるプラグイン。例としては、次を使用する Bazaar プラグインがあります。bzrlib
の遅延読み込みフレームワーク。
他のヒント
import ステートメントを関数内に置くと、循環依存関係を防ぐことができます。たとえば、X.py と Y.py という 2 つのモジュールがあり、両方とも相互にインポートする必要がある場合、モジュールの 1 つをインポートすると循環依存関係が発生し、無限ループが発生します。いずれかのモジュールで import ステートメントを移動すると、関数が呼び出されるまで他のモジュールのインポートは試行されず、そのモジュールはすでにインポートされているため、無限ループは発生しません。詳細についてはここをお読みください - effbot.org/zone/import-confusion.htm
私はすべてのインポートをモジュールの先頭ではなく、それらを使用する関数に置くという習慣を採用しました。
得られる利点は、より確実にリファクタリングできることです。関数をあるモジュールから別のモジュールに移動すると、その関数はテストの遺産をすべてそのままにして動作し続けることがわかります。モジュールの先頭にインポートがある場合、関数を移動するときに、新しいモジュールのインポートを完全かつ最小限に抑えるのに多くの時間を費やすことになります。リファクタリング IDE では、これが無関係になる可能性があります。
他の場所で述べたように、速度ペナルティがあります。私のアプリケーションでこれを測定したところ、私の目的にとっては重要ではないことがわかりました。
また、検索に頼らずにすべてのモジュールの依存関係を前もって確認できるのも便利です (例:grep)。ただし、モジュールの依存関係を気にする理由は、一般に、単一のモジュールだけではなく、複数のファイルで構成されるシステム全体をインストール、リファクタリング、または移動するためです。その場合は、とにかくグローバル検索を実行して、システムレベルの依存関係があることを確認します。したがって、実際のシステムを理解するのに役立つグローバルインポートは見つかりませんでした。
私は通常、次のインポートを入れます sys
の中で if __name__=='__main__'
チェックしてから引数を渡します(例: sys.argv[1:]
) に main()
関数。これにより、使用できるようになります main
という文脈で sys
輸入されていない。
ほとんどの場合、これは明確にするために役立ち、実行するのが賢明ですが、常にそうとは限りません。以下に、モジュールのインポートが別の場所に存在する可能性がある状況の例をいくつか示します。
まず、次の形式の単体テストを含むモジュールを作成できます。
if __name__ == '__main__':
import foo
aa = foo.xyz() # initiate something for the test
次に、実行時に条件付きで別のモジュールをインポートする必要がある場合があります。
if [condition]:
import foo as plugin_api
else:
import bar as plugin_api
xx = plugin_api.Plugin()
[...]
おそらく、コード内の他の部分にインポートを配置する状況が他にもあるでしょう。
関数が 0 回または 1 回呼び出される場合、最初のバリアントは 2 番目のバリアントよりも実際に効率的です。ただし、2 回目以降の呼び出しでは、「呼び出しごとにインポート」アプローチは実際には効率が低くなります。見る このリンク 「遅延インポート」を実行することで両方のアプローチの長所を組み合わせた遅延読み込み手法。
しかし、一方を他方よりも好む理由は、効率以外にもあります。1 つのアプローチは、コードを読む人に対して、このモジュールの依存関係をより明確にすることです。また、これらは非常に異なる失敗特性を持っています。前者は「datetime」モジュールがない場合ロード時に失敗しますが、後者はメソッドが呼び出されるまで失敗しません。
追加されたメモ: IronPython では、コードは基本的にインポート中にコンパイルされるため、インポートは CPython よりもかなり高価になる可能性があります。
カートは次のように良い点を指摘しています。2 番目のバージョンはより明確で、後でではなくロード時に予期せず失敗します。
通常、モジュールのロードの効率については心配しません。これは、(a) 非常に高速であり、(b) ほとんどの場合起動時にのみ行われるためです。
予期せぬタイミングで重量モジュールをロードする必要がある場合は、おそらく、 __import__
機能し、 もちろん 捕まえる ImportError
例外を考慮し、適切な方法で処理します。
モジュールを事前にロードする効率については、あまり心配する必要はありません。モジュールが占有するメモリはそれほど大きくなく (モジュールが十分にモジュール化されていると仮定して)、起動コストは無視できる程度になります。
ほとんどの場合、ソース ファイルの先頭にあるモジュールをロードする必要があります。コードを読む人にとって、どの関数やオブジェクトがどのモジュールから来たのかが非常に簡単にわかります。
コード内の別の場所にモジュールをインポートする十分な理由の 1 つは、それがデバッグ ステートメントで使用される場合です。
例えば:
do_something_with_x(x)
これを次のようにデバッグできます。
from pprint import pprint
pprint(x)
do_something_with_x(x)
もちろん、コード内の別の場所にモジュールをインポートするもう 1 つの理由は、モジュールを動的にインポートする必要がある場合です。というのは、ほとんど選択の余地がないからです。
モジュールを事前にロードする効率については、あまり心配する必要はありません。モジュールが占有するメモリはそれほど大きくなく (モジュールが十分にモジュール化されていると仮定して)、起動コストは無視できる程度になります。
これはトレードオフであり、プログラマーのみが決定できます。
ケース 1 では、必要になるまで datetime モジュールをインポートしない (および必要な初期化を行う) ことで、メモリと起動時間をいくらか節約します。「呼び出されたときのみ」インポートを実行するということは、「呼び出されるたびに」インポートを実行することも意味するため、最初の呼び出し以降の各呼び出しでは、インポート実行による追加のオーバーヘッドが依然として発生することに注意してください。
ケース 2 は、事前に datetime をインポートすることで実行時間とレイテンシを節約し、not_often_called() がより迅速に返されるようにします。 は また、呼び出しごとにインポートのオーバーヘッドが発生しないようにすることもできます。
効率に加えて、インポート ステートメントが次の場合、モジュールの依存関係を前もって確認しやすくなります。前に。コード内でそれらを隠すと、何かが依存しているモジュールを簡単に見つけることが難しくなる可能性があります。
個人的には、単体テストなど、常にロードしたくないものを除いて、通常は PEP に従っています。 知る テストコード以外には使用されません。
すべてのインポートが一番上にある例を次に示します (これを行う必要があるのは今回だけです)。Un*x と Windows の両方でサブプロセスを終了できるようにしたいと考えています。
import os
# ...
try:
kill = os.kill # will raise AttributeError on Windows
from signal import SIGTERM
def terminate(process):
kill(process.pid, SIGTERM)
except (AttributeError, ImportError):
try:
from win32api import TerminateProcess # use win32api if available
def terminate(process):
TerminateProcess(int(process._handle), -1)
except ImportError:
def terminate(process):
raise NotImplementedError # define a dummy function
(レビュー中:何 ジョン・ミリキン 言った。)
これは他の多くの最適化と同様、速度を上げるために読みやすさをある程度犠牲にします。John が述べたように、プロファイリングの宿題を完了し、これが非常に有用な変更であることがわかった場合は、 そして さらなるスピードが必要な場合は、それに挑戦してください。他のすべてのインポートについてメモを作成しておくとよいでしょう。
from foo import bar
from baz import qux
# Note: datetime is imported in SomeClass below
モジュールの初期化は、最初のインポート時に 1 回だけ行われます。問題のモジュールが標準ライブラリからのものである場合は、プログラム内の他のモジュールからもインポートする可能性があります。datetime と同じくらい普及しているモジュールの場合、他の多数の標準ライブラリとの依存関係もある可能性があります。モジュールの初期化がすでに行われているため、import ステートメントのコストはほとんどかかりません。この時点で実行しているのは、既存のモジュール オブジェクトをローカル スコープにバインドすることだけです。
可読性を高めるためにその情報と引数を組み合わせると、import ステートメントをモジュール スコープで使用するのが最善であると言えます。
ただ完了するだけ モエちゃんの答え そして元の質問:
循環依存に対処する必要がある場合、いくつかの「トリック」を行うことができます。 モジュールを操作していると仮定すると、 a.py
そして b.py
を含む x()
そしてb y()
, 、 それぞれ。それから:
- そのうちの 1 つを移動できます。
from imports
モジュールの下部にあります。 - そのうちの 1 つを移動できます。
from imports
実際にインポートを必要とする関数またはメソッドの内部 (複数の場所から使用する可能性があるため、これは常に可能であるとは限りません)。 - 2 つのうち 1 つを変更できます
from imports
次のようなインポートになります。import a
結論としては。循環依存関係を扱っておらず、それを回避する何らかのトリックを行っていない場合は、この質問に対する他の回答ですでに説明されている理由により、すべてのインポートを先頭に置くことをお勧めします。そして、この「トリック」を実行するときは、コメントを含めてください。いつでも大歓迎です。:)
すでに提供されている優れた回答に加えて、インポートの配置は単にスタイルの問題ではないことは注目に値します。場合によっては、モジュールに最初にインポートまたは初期化する必要がある暗黙的な依存関係があり、トップレベルのインポートが必要な実行順序の違反につながる可能性があります。
この問題は、Apache Spark の Python API でよく発生し、pyspark パッケージまたはモジュールをインポートする前に SparkContext を初期化する必要があります。SparkContext が利用可能であることが保証されているスコープに pyspark インポートを配置するのが最善です。
他の人がすでにこれを非常にうまく行っているため、私は完全な答えを提供するつもりはありません。関数内にモジュールをインポートするのが特に便利だと思われるユースケースを 1 つだけ説明したいと思います。私のアプリケーションは、プラグインとして特定の場所に保存されている Python パッケージとモジュールを使用します。アプリケーションの起動中、アプリケーションはその場所にあるすべてのモジュールを調べてインポートし、モジュール内を調べてプラグインのマウント ポイントが見つかったかどうかを確認します (私の場合、それは一意の属性を持つ特定の基本クラスのサブクラスです)。 ID)を登録します。プラグインの数は多く (現在は数十、将来的には数百になる可能性があります)、それぞれが使用されることはほとんどありません。プラグイン モジュールの先頭にサードパーティ ライブラリのインポートがあると、アプリケーションの起動時に少しペナルティが発生しました。特に一部のサードパーティ ライブラリはインポートが重いです (例:Plotly のインポートでは、インターネットに接続して何かをダウンロードしようとしても、起動に約 1 秒かかりました)。プラグインのインポートを最適化することで (インポートが使用される関数内でのみ呼び出します)、起動時間を 10 秒から約 2 秒に短縮することができました。それはユーザーにとって大きな違いです。
したがって、私の答えは「いいえ」です。インポートを常にモジュールの先頭に置く必要はありません。
予想されることについては多くの適切な説明があるにもかかわらず、繰り返しの負荷チェックにかかる実際のコストの数値が既に掲載されていないことに驚きました。
一番上でインポートすると、何があっても負荷がかかります。これはかなり小さいですが、通常はナノ秒ではなくミリ秒単位です。
関数内でインポートする場合、読み込みのヒットのみが取得されます。 もし そして いつ それらの関数の 1 つが最初に呼び出されます。多くの人が指摘しているように、そのようなことがまったく起こらなければ、ロード時間を節約できます。しかし、関数が頻繁に呼び出される場合、はるかに小さいヒットが繰り返し発生します (それをチェックするため) もっている ロードされました。実際に再ロードするためではありません)。一方、@aaronasterlingが指摘したように、関数内でインポートすると関数の使用がわずかに高速になるため、少しの節約にもなります。 ローカル変数 後で名前を特定するための検索 (http://stackoverflow.com/questions/477096/python-import-coding-style/4789963#4789963).
以下は、関数内からいくつかのものをインポートする簡単なテストの結果です。報告された時間 (2.3 GHz Intel Core i7 上の Python 2.7.14 で) を以下に示します (理由はわかりませんが、2 番目の呼び出しがそれ以降の呼び出しよりも多くの時間を費やしているように見えます)。
0 foo: 14429.0924 µs
1 foo: 63.8962 µs
2 foo: 10.0136 µs
3 foo: 7.1526 µs
4 foo: 7.8678 µs
0 bar: 9.0599 µs
1 bar: 6.9141 µs
2 bar: 7.1526 µs
3 bar: 7.8678 µs
4 bar: 7.1526 µs
コード:
from __future__ import print_function
from time import time
def foo():
import collections
import re
import string
import math
import subprocess
return
def bar():
import collections
import re
import string
import math
import subprocess
return
t0 = time()
for i in xrange(5):
foo()
t1 = time()
print(" %2d foo: %12.4f \xC2\xB5s" % (i, (t1-t0)*1E6))
t0 = t1
for i in xrange(5):
bar()
t1 = time()
print(" %2d bar: %12.4f \xC2\xB5s" % (i, (t1-t0)*1E6))
t0 = t1
興味深いのは、これまで並列処理について言及した回答が 1 つもなかったことです。シリアル化された関数コードが他のコアにプッシュされている場合、インポートが関数内にあることが必須になる可能性があります。ipyParallel の場合と同様です。
変数/ローカルスコープを関数内にインポートすると、パフォーマンスが向上する可能性があります。これは、関数内でインポートされたものの使用方法によって異なります。何度もループしてモジュール グローバル オブジェクトにアクセスしている場合は、それをローカルとしてインポートすると役に立ちます。
テスト.py
X=10
Y=11
Z=12
def add(i):
i = i + 10
runlocal.py
from test import add, X, Y, Z
def callme():
x=X
y=Y
z=Z
ladd=add
for i in range(100000000):
ladd(i)
x+y+z
callme()
run.py
from test import add, X, Y, Z
def callme():
for i in range(100000000):
add(i)
X+Y+Z
callme()
Linux での時間はわずかな増加を示しています
/usr/bin/time -f "\t%E real,\t%U user,\t%S sys" python run.py
0:17.80 real, 17.77 user, 0.01 sys
/tmp/test$ /usr/bin/time -f "\t%E real,\t%U user,\t%S sys" python runlocal.py
0:14.23 real, 14.22 user, 0.01 sys
本物は壁掛け時計です。ユーザーはプログラムに参加している時間です。sys はシステムコールの時間です。
https://docs.python.org/3.5/reference/executionmodel.html#resolution-of-names
@John Millikin と @V.K. が言及したユースケースと非常によく似た、私のユースケースについて触れたいと思います。:
オプションのインポート
私は Jupyter Notebook を使用してデータ分析を行っており、すべての分析のテンプレートとして同じ IPython ノートブックを使用しています。場合によっては、モデルを簡単に実行するために Tensorflow をインポートする必要がありますが、tensorflow がセットアップされていない場所やインポートが遅い場所で作業することもあります。そのような場合、私は Tensorflow に依存する操作をヘルパー関数にカプセル化し、その関数内に tensorflow をインポートして、ボタンにバインドします。
こうすることで、インポートを待たずに、または失敗したときに残りのセルを再開する必要もなく、「再起動してすべて実行」を実行できます。