読者です 読者をやめる 読者になる 読者になる

django.utils.functionalを使って遅延評価

Python

 django.utils.functionalに含まれるlazyを使うと、関数の遅延評価を簡単に行うことができます。
 カレー化を行うdjango.utils.functional.curryはPython2.5のfunctoolsでも代替可能でしたが、このlazyは標準ライブラリの中に代替がなく(多分)、かつ、非常に有用な関数です。


 Pythonでは、「ある条件が満たされない場合、別の関数を評価する」というパターンはしばしば見られます。
 典型的なものとしては、キーがない場合に返すべき値と共に呼び出す、dictのget, setdefault, popメソッドなどです。
 少々トリビアルな例なのですが、遅延評価の効果を体感するために、次のような例を考えてみました。

import time

def get_value(v):
    time.sleep(3)
    return v

 このget_value関数は、呼び出されると3秒間休止した後、引数をそのまま返します。この関数とdictのgetメソッドを組み合わせて使ってみます。

>>> d={}
>>> value = d.get('spam', get_value('spam'))

 実行してみると、3秒後に結果が返ってきます。次に、指定したキーがある場合の動作を確認します。

>>> d['spam'] = 'Spam'
>>> value = d.get('spam', get_value('spam'))

 今度は指定したキーに対応する値があるのですが、やっぱり3秒後に結果が返ってきます。これはPythonにおける関数の引数の評価の仕方が、事前に引数を評価して、それから関数本体を評価する、という順序になっているためです。
 get, setdefault, popといったメソッドを上手く使うとコードが簡潔になるのですが、反面、デフォルトの値として関数の返り値を与えたい場合などは無駄が多くなってしまいます。そのため、泣く泣く、次のように書き換えたりするわけです。(え、しないって?)

if key in d:
    value = d[key]
else:
    value = d[key] = get_value(key)

 このようの状況でlazyを使うと、非常に効果があります。lazyは次のような感じで使用します。

from django.utils.functional import lazy

get_value_lazy = lazy(get_value, str, unicode)

 lazyの第一引数に遅延評価の対象となる関数f、第二引数以下に関数fの返す値の型を指定します。このようにしてlazyでデコレートした関数を使って、上で行ったgetの例をもう一度実行してみます。

>>> d={}
>>> value = d.get('spam', get_value_lazy('spam'))
# 3秒後に結果が返ってくる
>>> d['spam'] = 'spam'
>>> value = d.get('spam', get_value_lazy('spam'))
# すぐに結果が返ってくる
>>> value
# 3秒後に結果が返ってくる
'spam'

 このget_value_lazyが返り値は、文字列と同じように振る舞いますが、実際はProxyオブジェクト(django.utils.functional.__proxy__)、あるいは、標準ドキュメントで使われている用語でいえば「遅延参照(lazy reference)」で、本当に値が必要になるまで、関数の評価は行われません。
 最初にgetを実行したときには、まだキーがないので、getメソッドは、デフォルトの値を返すために、第二引数の遅延参照を実際に評価します。
 dictにキーと値を設定してから、同じことを繰り返すと、この場合は遅延参照が評価されないため、今度はすぐに結果が返ってきます。ここが遅延評価を行っていない最初の例との違いです。


 djangoは他のWebフレームワークと比べて、実感としても、ベンチマークの結果(参考URL:Framework Performance)を見ても、もの凄く速いのですが、このlazyに見られるのような細かなテクニックもパフォーマンス向上の寄与していると思われます。
 具体的に言うと lazyは国際化関連のコードの中で使われていて、標準ドキュメントの中にも、その考え方が示されています。


 ただ、何もかも遅延評価を行えばよいかと言えば、そうではなく、何度も参照される値の場合は、遅延評価を行わない方が効率が上がります。これは、遅延参照では、値が参照されるたびに、デコレートされた関数が呼び出されるためです。従って、次のようなコードは、lazyの使用法の悪い例です。

def do_some_task():
    value = get_value_lazy('spam')
    a = value + ' egg'
    b = value + ' ham egg'
    c = value + ' spam egg'
    return ','.join((a, b, c))

 この例では、get_value_lazyの返したProxyオブジェクトを、3回参照しており、参照されるたびに、元の関数get_valueが呼び出されることになります。このようなケースで、遅延評価を行うのは誤りです。


 curry, lazyを含むdjango.utils.functionalのコードは、たった55行の非常に短いコードですが、非常にPythonらしいスタイルで書かれ、かつ、純粋関数型言語の考え方も取り込んだ、大変興味深いコードです。djangoに興味がある・なしに関わらず、Pythonを勉強されている方は一度目を通されることをおすすめします。