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 に、たまに引っかかるようです。