私はまだ Python に不慣れで、Python スクリプトのパフォーマンスを改善しようとしてきたので、グローバル変数を使用して、または使用せずにテストしました。タイミングを計ったところ、驚いたことに、ローカル変数を関数に渡すよりもグローバル変数を宣言した方が高速に実行されました。どうしたの?ローカル変数の方が実行速度が速いと思った?(グローバルが安全でないことはわかっていますが、まだ興味があります。)
4 に答える
簡単な答え:
Python の動的な性質により、インタープリターは abc のような式に遭遇すると、(最初にローカル名前空間、次にグローバル名前空間、最後に組み込み名前空間を試します) を調べ、次にそのオブジェクトの名前空間を調べて解決します。名前 b を検索し、最後にそのオブジェクトの名前空間を調べて名前 c を解決します。これらのルックアップはかなり高速です。ローカル変数の場合、インタープリターはどの変数がローカルであるかを認識し、メモリ内の既知の位置を割り当てることができるため、ルックアップは非常に高速です。
インタープリターは、関数内のどの名前がローカルであるかを認識し、関数呼び出しのメモリ内の特定の (既知の) 場所を割り当てます。これにより、ローカルへの参照が、グローバルや (特に) ビルトインへの参照よりもはるかに高速になります。
同じことを説明するコード例:
>>> glen = len # provides a global reference to a built-in
>>>
>>> def flocal():
... name = len
... for i in range(25):
... x = name
...
>>> def fglobal():
... for i in range(25):
... x = glen
...
>>> def fbuiltin():
... for i in range(25):
... x = len
...
>>> timeit("flocal()", "from __main__ import flocal")
1.743438959121704
>>> timeit("fglobal()", "from __main__ import fglobal")
2.192162036895752
>>> timeit("fbuiltin()", "from __main__ import fbuiltin")
2.259413003921509
>>>
Python が関数をコンパイルするとき、関数は呼び出される前に、変数がローカル、クロージャー、またはグローバルのいずれであるかを認識します。
関数で変数を参照するには、いくつかの方法があります。
- グローバル
- 閉鎖
- 地元の人々
それでは、これらの種類の変数をいくつかの異なる関数で作成して、自分で確認できるようにしましょう。
global_foo = 'foo'
def globalfoo():
return global_foo
def makeclosurefoo():
boundfoo = 'foo'
def innerfoo():
return boundfoo
return innerfoo
closurefoo = makeclosurefoo()
def defaultfoo(foo='foo'):
return foo
def localfoo():
foo = 'foo'
return foo
分解した
各関数が変数を検索する場所を知っていることがわかります - 実行時にそうする必要はありません:
>>> import dis
>>> dis.dis(globalfoo)
2 0 LOAD_GLOBAL 0 (global_foo)
2 RETURN_VALUE
>>> dis.dis(closurefoo)
4 0 LOAD_DEREF 0 (boundfoo)
2 RETURN_VALUE
>>> dis.dis(defaultfoo)
2 0 LOAD_FAST 0 (foo)
2 RETURN_VALUE
>>> dis.dis(localfoo)
2 0 LOAD_CONST 1 ('foo')
2 STORE_FAST 0 (foo)
3 4 LOAD_FAST 0 (foo)
6 RETURN_VALUE
LOAD_GLOBAL
現在、グローバルのバイトコードは、クロージャ変数はLOAD_DEREF
、ローカルは であることがわかりますLOAD_FAST
。これらは CPython の実装の詳細であり、バージョンごとに変更される可能性がありますが、Python が各変数ルックアップを異なる方法で処理することを確認できると便利です。
インタープリターに貼り付けて、自分の目で確かめてください。
import dis
dis.dis(globalfoo)
dis.dis(closurefoo)
dis.dis(defaultfoo)
dis.dis(localfoo)
テストコード
テストコード (お使いのシステムで自由にテストしてください):
import sys
sys.version
import timeit
min(timeit.repeat(globalfoo))
min(timeit.repeat(closurefoo))
min(timeit.repeat(defaultfoo))
min(timeit.repeat(localfoo))
出力
Windows では、少なくともこのビルドでは、クロージャが少しペナルティを受けるように見えます。毎回ローカルを割り当てる必要がないため、デフォルトのローカルを使用するのが最速です。
>>> import sys
>>> sys.version
'3.6.1 |Anaconda 4.4.0 (64-bit)| (default, May 11 2017, 13:25:24) [MSC v.1900 64 bit (AMD64)]'
>>> import timeit
>>> min(timeit.repeat(globalfoo))
0.0728403456180331
>>> min(timeit.repeat(closurefoo))
0.07465484920749077
>>> min(timeit.repeat(defaultfoo))
0.06542038103088998
>>> min(timeit.repeat(localfoo))
0.06801849537714588
Linux の場合:
>>> import sys
>>> sys.version
'3.6.4 |Anaconda custom (64-bit)| (default, Mar 13 2018, 01:15:57) \n[GCC 7.2.0]'
>>> import timeit
>>> min(timeit.repeat(globalfoo))
0.08560040907468647
>>> min(timeit.repeat(closurefoo))
0.08592104795388877
>>> min(timeit.repeat(defaultfoo))
0.06587386003229767
>>> min(timeit.repeat(localfoo))
0.06887826602905989
テストする機会があるので、他のシステムを追加します。