Diary?

2009-07-10
Fri

(13:45)

例えば次のような関数のテストをしていたとして、その作業は何らかのタスク管理システムに登録されているものとする。

import bar
def foo(arg):
    a = do_something(arg)
    b = bar.buz(a)
    ...

それで

  • bar モジュールにバグ(テスト漏れ)が見つかった
  • 自分が bar モジュールの担当者でもあった

という時に、 foo のテストと bar の修正を同時にコミットするのは好ましくないというか問題大有り。どうしてかというと、事前に登録されたタスク以上の事をコミットに含めてしまっているので、タスク管理システム側からは「bar モジュールに問題があった」という事実が見えなくなってしまうから。もちろんコミットログを見れば一目瞭然かもしれないけど、でもコミットログとタスク管理の間に齟齬があるのって事故の元だと思わない?

俺は今一人で開発してるし、何よりプロトタイプ的な意味合いもあるのでそこまで神経質にならなくても(今のところ)問題ないけど、でも本当にトレーサビリティを気にするのなら、コミットを細かく分けるとか一つのコミットに複数のタスクやチケットを紐付けて管理するとか、そういう配慮が必要だよな。

ちなみに darcs はコミットをしようとするとファイル単位どころか同じファイルの中でも変更点が切り分けられて(まあ連続した行への変更をまとめてるだけっぽいが)、それを対話的に選ばせるというインターフェースになっている。かったるいといえばそうかもしれないけど、でもこっちの方が理屈の上では健全な気もする。

(15:11)

Python では動的にモジュールオブジェクトを変更したり生成したりできるのは常識なので、次のようなことが可能となる。

  1. 既存のモジュールで公開されている関数にラッパー関数を適用
  2. そうしてできた関数群をモジュールのアイテムとする新たなモジュールを生成

簡単に書くと、こんな感じ。

import foo
funcs ={
    'bar': some_higher_order_function(foo.bar)
    'buz': some_higher_order_function(foo.buz)
}
mod = types.ModuleType('newmod')
mod.__dict__.update(funcs)

ここまでは普通にできる。それじゃ、ここでラップしてる foo モジュールの中身がこうなっていたらどうしよう。

def bar(arg):
    ...
    buz() # 内部で buz を呼んでる
    ...
def buz(arg):
    ...

この時 bar の中で参照している buz の実体はラップされた後の bar ではなく、ラップされる前の foo モジュールにおける buz となる。当たり前だとしかいいようがないが、時々参照先もラップした後の関数になっていてほしい事がある。例えばメモ化を後入れする時がそうだ。メモ化を次のような高階関数として実装したとする(わかってると思うけどこれ手抜きだからな。コピペして使ったりするなよ)。

def memoize(f):
    cache = {}
    def wrapper(arg):
        if (f, arg) in cache:
            return cache[(f, arg)]
        else:
            r = f(arg)
            cache[(f, arg)] = r
            return r
    return wrapper

最初に例示したコードのような手法を使えば全てのモジュール関数がメモ化されたモジュールを生成することができるが、これが上手く行くためにはある関数から別の同一モジュール内の関数を呼んでいない時に限られる。先の foo モジュールを例にもう一度コード書くと、

import foo
funcs ={
    'bar': memoize(foo.bar)
    'buz': memoize(foo.buz)
}
mod = types.ModuleType('newmod')
mod.__dict__.update(funcs)

ここで mod から bar と buz を呼び出す限りそれらはメモ化された関数なのだが、その mod.bar から呼ばれる buz はメモ化される前の foo.buz のままになる。一応ラップされた関数から func_closure と func_globals を辿り元の関数の func_globals を書き換えるという大技があるにはあるが、だったら元のモジュールの辞書を直に update するのと同じじゃんという話だ。

いっそ copy.deepcopy でモジュールをまるごとコピーして中身を書き換えようかとも思ったが、そういや copy.deepcopy 関数はモジュール、関数、スタックフレームなどを扱えないのだった。なので特定のモジュール全体をラップして、なおかつモジュール内部の参照もラップ後の関数で置換したい場合、諦めてモジュールそれ自体のアイテムを更新してしまい、その処理を行う関数をメモ化するか再入不能にして、何度もラップ処理が走らないようにするしかなさそうである。

まあ、そこまでしなくてもいいような設計にするというのが一番の正解だとは思うけどな。

Creative Commons
この怪文書はクリエイティブ・コモンズ・ライセンスの元でライセンスされています。引用した文章など Kuwata Chikara に著作権のないものについては、それらの著作権保持者に帰属します。