Pythonコード添削道場 [Main] 続:typoを素早く察知する方法

PythonのバイトコードをGRINEditで可視化[Python(18)]続:typoを素早く察知する方法

PythonのバイトコードをGRINEditで可視化[下を行け(3)]

Pythonでtypoを素早く察知する方法

先日「スクリプト言語は便利だけど、 長時間計算した後で綴り間違い(typo)で落ちたりすると やる気がなくなるよね」 という話を聞きました。 たしかに、綴り間違いで落ちるのは悲しいです。 せめて「代入されたけども読み出されていない変数」とか 「代入される前に読み出されている変数」が出力できれば、 早めに綴り間違いに気づくことができますね。

そこで「先日公開したCodeHackモジュールを使えば簡単にできるよ」と言いながら作ったのが今回紹介するモジュールです。 CodeHack 0.05に同梱しました。 CodeHack 0.06に同梱しました。

使い方は簡単です。 たとえば下のような関数fooを定義したとします。

def foo(left, top, right, bottom):
    "function for test"
    width = right - left
    hieght = top - botom
    print height, width
よく見ると2カ所ほど綴り間違いがあるのですが、 この関数をcodehack.var_warningの中のcheckfuncに渡してみると 下のようにその綴り間違いの変数が表示されます。関数fooを実行していないので、たとえ実行に1時間かかるような関数であってもチェックは一瞬です。
>>> from codehack.var_warning import checkfunc
>>> checkfunc(foo)
variable 'botom' is global
variable 'height' is global
variable 'hieght' is not used
variable 'bottom' is not used
実は綴り間違いの判定は何もしていなくて、 単純に「値が代入されたのに読み出されていない」 という変数を「not used」と表示しているだけです。 また、逆の「値が代入されていないのに読み出されている変数」は Pythonでは「グローバル変数と見なして外のスコープを見に行く」 という仕様になっているのでglobalと表示しています。(注1)

ただ、そのままですと'range'や'set'なども警告されたり、 自分で定義したグローバル関数を使うとそれも警告されたり、 とうっとうしいことになるので、 警告しない名前のリストをsurpressという引数で指定できるようにしてあります。 デフォルトでは組み込みの名前(rangeとか)がsurpressに入っています。

def checkfunc(func,
          print_globals = True,
          print_not_used = True,
          print_func_name = False,
          surpress = _default_surpress):

CodeHackモジュールを使っている部分は、実はたいしたことはしていません。 下のようにLOAD_FAST(ローカル変数のロード)という オペコードの引数(変数名になっている)を集めているだけです。 codehack.util.analyzeはオペコード間の依存関係(どのオペコードが積んだ値をどのオペコードが消費するか)のトレースをするのですが、それも必要ないのでわざわざ外しています。

    import util
    opcodes = util.analyze(func, do_trace=False)

    loaded_vars = set(op.argstr
                for op in opcodes
                if op.opname == "LOAD_FAST")
下のようにして、モジュールの中の全ての関数をチェックすることもできます。 すでに自分で書いたモジュールがあれば試してみると面白いでしょう。 checkallはprint_globalsをFalseにしてあるので「代入されているけど読み出されていない」変数だけが出ます。
>>> from codehack.var_warning import checkfunc, checkall
>>> import urllib
>>> checkfunc(urllib.urlopen)
variable 'FancyURLopener' is global
variable '_urlopener' is global
>>> checkall(urllib.__dict__)
* function splitnport
* function getproxies
* function retrieve
variable 'msg' is not used
variable 'garbage' is not used
(snip)
ぜひ試してみて、ご意見ご感想やパッチを頂けるとうれしいです。

(注1) 実際はもちろんグローバルの名前空間にいきなり行くわけではなくて、 スコープがネストしている場合には1枚外のスコープへ行きます。 globalと呼んでいるのは単にバイトコードがLOAD_GLOBALだからだったりします。 いい名称があれば変えたほうがいいかもしれません。 また、関数がネストしている場合は内側の関数で使われる変数を 「使われる」と判断することができずに警告を出してしまいます。 これはdisしてみるとわかるのですが、内側の関数はコードオブジェクトとして 定数テーブルに埋め込まれているため、 外側の関数のバイトコードを見ただけでは内側で何を使うかがわからない ことが原因です。定数テーブルにコードオブジェクトがあるかどうかをチェックして 再帰的にたどるようにすればいいだろうと思いますが…それは今後の反響次第と言うことで。

フィードバック

by 鮎川 | 2007年08月17日 10:16

codehack0.6を使っているのですが、Unicodeの日本語定数が自コードに記述されていると、codehack/definition.pyの41行目の

self.argstr = str(self.cobj.co_consts[self.arg])
でstrに変換できずにエラーとなるようです。 (self.cobj.co_consts[self.arg]にUnicodeの日本語文字列が来る。) ※自コードはeuc-jpで書かれており、ちょうど以下の部分の"""で囲まれた引数をcodehackが処理しようとして落ちています。
import optparse
parser = optparse.OptionParser(usage=u"""
    KW抽出。
    """)
今のところUnicodeの場合は適当に別処理にするように修正して手元で使っていますが、codehackの仕組みがよくわかっていないのでお時間のあるときに公式な修正をよろしくおねがいしますっ!

ご意見・ご感想をお送りください(フィードバック)

(フィードバックはメールで送信され、基本的に表示されませんが、内容によっては公開させていただくこともございます。ご了承ください。Your comment doesn't appear the page immediately. If the comment has value to other people, it will be put on the page or subsequent entries. Thank you.)

上の情報は、いずれも未記入でかまいません。 All of above questions are optional.