Pythonのhasattr()は遅い?

Pythonには、オブジェクトにある名前の属性が存在するかどうかをチェックする hasattr という組み込み関数があります。

例えば、リストオブジェクトに append という属性が存在するかどうか確認するときは、次のようにかきます。

In [57]:
L = []
print(hasattr(L, 'append'))
print(L.append)
True
<built-in method append of list object at 0x7fbc80542d80>

リストオブジェクトには append という属性が存在し、メソッドだということがわかります。

もう一つ、appppend という属性があるかどうか調べてみましょう。

In [59]:
L = []
print(hasattr(L, 'appppend'))
print(L.appppend)
False
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-59-1cbdd23b05f2> in <module>
      2 L = []
      3 print(hasattr(L, 'appppend'))
----> 4 print(L.appppend)

AttributeError: 'list' object has no attribute 'appppend'

appppend 属性は存在しないようです。

hasattr() は遅い?

ところで、この hasattr()ドキュメント には、次のように書かれています。

この関数は、 getattr(object, name) を呼び出して AttributeError を送出するかどうかを見ることで実装されています。

この説明を読むと、hasattr() は次のように実装されているように思えます。

In [50]:
def my_hasattr(object, name):
    try:
        getattr(object, name)
        return True
    except AttributeError:
        return False

getattr で属性値を取得し、正常に取得できれば True を、AttributeError 例外が発生すれば False を返します。

これはドキュメントに記載されている通りの実装ですが、見るからに遅そうです。例外処理というのは比較的重たい処理で、例外の発生を検出したら実行情報を保存し、適切な except ブロックに移動して処理を継続できるようにしなければなりません。

存在しない属性がたくさんあるようなケースでは、AttributeError が大量に発生するためにhasattr()は遅くなってしまいそうです。上の my_hasattr() を使って実験してみましょう。

まず、属性が存在する場合を測定してみます。

In [60]:
%%time
L = []
for i in range(10000):
    my_hasattr(L, 'append')
CPU times: user 2.91 ms, sys: 0 ns, total: 2.91 ms
Wall time: 2.92 ms

同様に、存在しない属性を調べてみましょう。

In [61]:
%%time
L = []
for i in range(10000):
    my_hasattr(L, 'appppend')
CPU times: user 8.26 ms, sys: 0 ns, total: 8.26 ms
Wall time: 8.16 ms

予想通り、存在しない属性のチェックは約2倍の時間がかかっています。

では、それぞれのケースを、本物の hasattr() で調べてみましょう。

In [62]:
%%time
L = []
for i in range(10000):
    hasattr(L, 'append')
CPU times: user 1.56 ms, sys: 0 ns, total: 1.56 ms
Wall time: 1.56 ms
In [63]:
%%time
L = []
for i in range(10000):
    hasattr(L, 'appppend')
CPU times: user 1.54 ms, sys: 0 ns, total: 1.54 ms
Wall time: 1.54 ms

これはしたり。本物の hasttr では、どちらもほとんど差がありません。なんなら例外が発生している方がちょっと速くなってしまっています。

hasattr() の仕組み

my_hasattr() の実験を見て分かる通り、try-except を使った例外処理はやや時間のかかる処理です。しかし、実は例外を発生されるのはそんなに時間がかかりません。Pythonインタープリタが発生した例外を検出し、例外情報を作成したりする処理は時間がかかりますが、発生させるだけならほとんど時間はかからないのです。

my_hasattr(L, 'appppend') のように存在しない属性をチェックすると、次のように処理が行われます。

  1. my_hasattr()getattr(L, 'appppend') を呼び出す。
  2. getattr() 内部で AttributeError が発生し、例外情報を設定する。
  3. Pythonインタープリタが例外の発生を検出し、情報を整理して except AttributeError: に移動する。
  4. return False

ここで、時間がかかるのは 3. の例外が発生したあとの処理で、2. の例外の設定そのものは、かなり短時間で終了します。

ところで、hasattr() はPythonではなく、C言語で書かれているので、発生した例外をインタープリタに見つからないように消してしまえます。擬似的なPythonで書くと、次のようになっています。

# Pythonで擬似的に書いたhasattrの実装
def hasattr(object, name):
    # getattr()を呼び出す
    getattr(object, name)

    # 例外が発生しているか
    if is_exception_raised():

        # 例外はAttributionErrorか
        if exception_is_attributeerror():

            # 例外をクリア
            clear_exception()

            # Falseを返す
            return False

    return True

このようにすることで、getattr() で発生した例外をPythonインタープリタに見つかる前に消してしまうため、負荷の大きい例外処理を行わずに取得した結果だけを利用できます。このため、AttributionError 例外が発生してもしなくても、例外処理をおこなうことなく、同じような負荷で処理できています。

2021/1/8

最近の実装を見ずに記憶だけでこの記事を書いていましたが、@methaneさんの チューニング が入っていて、サンプルに使っていた datetime.datetime のようなオブジェクトの場合は、通常の getattr とはちょっと違う処理が行われるように変更されています。

このため、サンプルコードで使っていたオブジェクトを datetime.datetime からリストオブジェクトに変更しましいた。

Amazon.co.jpアソシエイト: