Last Updated: 2/6/2024, 5:44:57 AM
# 2. 変数の参照と属性の参照
変数の参照の方が速い
# 2.1. インスタンス変数の参照
# 2.1.1. 比較対象
# 属性の参照
obj.attr
# 変数の参照(一旦変数に格納する。)
# var = obj.attr
var
# 2.1.2. 測定結果
2倍くらい速かったりします。
# 対話モード >>> にコピペで実行できます。
import timeit
class Cls:
pass
obj = Cls()
class Slt:
__slots__ = ['attr']
slt = Slt()
obj.attr = 0
slt.attr = 0
var = 0
timeit.timeit('obj.attr', globals=globals())
timeit.timeit('slt.attr', globals=globals())
timeit.timeit('var', globals=globals())
timeit.default_number
>>> timeit.timeit('obj.attr', globals=globals())
0.06377833999999893
>>> timeit.timeit('slt.attr', globals=globals())
0.05894674500000008
>>> timeit.timeit('var', globals=globals())
0.029986893000000236
>>>
# 2.1.3. 特殊属性 __slots__
上の結果を見ると変数 var
の参照が最も速いことがわかりました。
属性の参照では slt.attr
の方が obj.attr
よりも、ほんの少しですが速かったです。
特殊属性 __slots__
を定義することでメモリや属性参照の速度が改善します。
3.3.2.4. __slots__ (opens new window)
__dict__ を使うのに比べて、 節約できるメモリ空間はかなり大きいです。 属性探索のスピードもかなり向上できます。
性能が上がる反面、制限があります。新しく属性を定義することができなくなります。
class Slt:
__slots__ = ['attr']
slt = Slt()
slt.attr = 0
slt.attr = 1 # 変更はできる
slt.new_attr = 2 # 追加はできない
>>> slt.new_attr = 2 # 追加はできない
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Slt' object has no attribute 'new_attr'
>>>
# 2.2. メソッドの参照
# 2.2.1. 比較対象
メソッドを変数に代入すると速くなる。
# メソッドの参照
lst.pop()
# 変数の参照(一旦変数に格納する。)
# pop = lst.pop
pop()
# 2.2.2. 測定結果
pop メソッドの実行時間が大きいので、 純粋に属性を参照したときに2倍速くなる、みたいなことはないですね。
# 変数の参照
python -m timeit -s "pop=[i for i in range(10000000)].pop" "pop()"
# メソッドの参照
python -m timeit -s "lst=[i for i in range(10000000)]" "lst.pop()"
$ # 変数の参照
$ python -m timeit -s "pop=[i for i in range(10000000)].pop" "pop()"
5000000 loops, best of 5: 76.5 nsec per loop
$
$ # メソッドの参照
$ python -m timeit -s "lst=[i for i in range(10000000)]" "lst.pop()"
2000000 loops, best of 5: 101 nsec per loop
$
# 2.2.3. 補足
メソッドを参照すると、属性の参照だけではなく、 関数からメソッドに変換するという処理も合わせて走っています。 変数に代入することで、その処理もなくすことができます。
メソッドを変数に代入したときの動作はこんな感じです。
# コピペで実行できます。
lst = [0, 1, 2, 3, 4]
pop = lst.pop
pop()
pop()
pop()
pop()
pop()
pop()
lst # <- リストは空っぽになる。
>>> pop()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: pop from empty list
>>> lst # <- リストは空っぽになる。
[]
>>>
# 2.3. 1次元リストと2次元リストの参照
答え: 二次元リストの参照の方が速い
# 2.3.1. 比較対象
2 次元リストと 2 次元リストを 1 次元リストで表現したものの参照速度を比較してみます。
# 2 次元リストを 1 次元リストで表現したもの
lst1[n*row + col]
# 2 次元リスト
lst2[row][col]
2次元リストを1次元で表現するのは、 競技プログラミングのコードを見ていたらでてきたコードです。 C 言語などでは1次元リストで表現した方が速いそうです。 Python では、どうでしょうか。
# 2.3.2. 測定結果
# 2 次元リストを 1 次元リストで表現したもの
python -m timeit -s "lst=[None]*3000*3000;row=1500;col=1500;N=3000" "lst[row*N+col]"
# 2 次元リスト
python -m timeit -s "lst=[[None]*3000 for i in range(3000)];row=1500;col=1500" "lst[row][col]"
$ python -m timeit -s "lst=[None]*3000*3000;row=1500;col=1500;N=3000" "lst[row*N+col]"
2000000 loops, best of 5: 107 nsec per loop
$ # 2 次元リスト
$ python -m timeit -s "lst=[[None]*3000 for i in range(3000)];row=1500;col=1500" "lst[row][col]"
5000000 loops, best of 5: 70.2 nsec per loop
$
# 2.3.3. 注意事項
-s
オプションで row, col を指定しておかないと1次元リストの場合、
最適化されて結果が逆転されてしまいます。
詳細は timeit のページで記述させていただきました。
# 2.4. ローカル変数とグローバル変数(追記)
ローカル変数の方が速いです。 TLE に、たまに引っかかるようです。