pythonで__line__を使う

データの定義位置を取得したいみたいなことを清水川さんのページで見た。pythonにはC言語のマクロみたいに__line__がないので、frameオブジェクトからファイル行数取ってとかを関数でやるのが一般的みたい。また、atsuoishimotoの日記の記事では、簡易DSLみたいな感じで実装してる。個人的には行数を取得するぐらいだったら清水川さんがやってるようにファクトリー関数でラップするのはありだと思う。なんだけど、ただ行数が欲しいだけだったらそもそも__line__があればいいんだよ!!ということでチャレンジしてみた。最初はインタプリターいじらないとだめかな〜と思ってたけど、全然そんなことはなかった。

で、作ってみたコードがこれ。

# -*- coding: utf-8 -*-

def trace(frame, event, arg):
    if event == 'line':
        frame.f_globals['__line__'] = frame.f_lineno
    return trace

def apply(module):
    if hasattr(module, 'settrace'):
        module.settrace(trace)
    
if __name__ == '__main__':
    import sys

    args = sys.argv[1:]
    if len(args) == 1 and args[0].endswith('.py'):
        apply(sys)
        execfile(args[0])

tracebackの表示内容を変えるとかそんな用途には使えないけど、__line__が使いたいだけだったらこれで十分かな?意外と綺麗にまとまった気がします。

使い方はコマンドラインから

python lineon.py somefile.py

とするか、

# -*- coding: utf-8 -*-                                                         

import sys
import lineon 

lineon.apply(sys)                                                                   

def gendata():
    return [{'data':'hoge', 'line': __line__},
            {'data':'fuga', 'line': __line__}]

data = gendata()

print data

のようにするだけ。ただ、一つ注意点があって、コードの中でlineon.applyを呼ぶ場合は__line__は関数の中でしか使えないという点。困ったことにglobal領域では動かない。global領域でも__line__を使いたい場合はコマンドラインからやる必要がある。なんか実装の仕方があると思いますが、そこまでpython詳しくないのでわからないw+調べてないw+そこは誰かに期待wせっかくだからpypiにでも登録してみようかな?python hack-a-thonで習ったし。あ、それと本番用コードにこれを使うのもおすすめできません。関数コールとかの度にフックするんで速度低下を招きます。普段使いも__debug__とかで使い分けること推奨。(なんだかC言語チックですがw)

そんでもって、どうやって実現しているかというとsysモジュールまたはthreadingモジュールのsettrace関数を使ってます。このsettraceというのはインタープリターを動的にフックするようにできる関数で、デバッガ(pdb)なんかで使われているようです。言語的にデバッガを支援してるのは素晴らしいですね。いっそのこと、IDE支援もやって欲しい気がしますが。コード補完支援とかコード補完支援とかコード補完支援とかwこのsettraceを使うと関数の呼び出しのタイミングでフックできたり、新しい行を実行するときにフックしたり、例外時や値返却時などのタイミングでフックできます。詳しくはPython Documentをご覧ください。この関数を使えばプロファイラとかコードカバレッジでも、簡単なものなら比較的楽に作れそうな気がしないでもないですね。