« 2007年05月 | メイン | 2007年08月 »

2007年06月 アーカイブ

2007年06月04日

コーディング過程をLingrで中継

lingr.png

右半分がPythonの対話的インタプリタIdleで、左半分がチャットのLingrです。 対話的にコードを書いて試している過程がチャットでどんどん公開されていきます。

Lingrのボット(チャットを読み書きするプログラム)は inforno :: Python版Lingr APIライブラリ を使うと簡単に書けます。 リンク先の例では入退室と発言しかありませんが、 他人の発言をウォッチするのも User Observe in Lingr Developer Wiki を使うだけで簡単にできます。

しかし、Lingrにコードを書くのはオートインデントとかがなくて面倒なので、 対話的インタプリタの側からLingrに「実行する内容」と「実行した結果」を送信するようにしました。 チャットに書いたコードが自動的に実行されるとなるとセキュリティが心配ですが、 これは自分のマシンで走るのは自分の書いたコードだけなので安心です。

一番時間がかかったのは実は 「コンパイル時に"single"と指定すれば、与えられたコードが式である場合に評価した結果を表示するバイトコードが付加される」 と気づくところです。 ライブラリリファレンスの組み込み関数のところに書いてあるのに気がつかず、PyShell.pyを読みながら 「execはあるけども式かどうか判別するコードも評価した値を表示するコードもないぞ?」 と悩んでいたのでした。まさかバイトコードに入っているとは。

下のコードはまだ荒削りですが、 とりあえずこれで上図のような結果が出ます。 複数行のコードの場合、インデントがデフォルトでは表示されませんが、 これはLingrの方で「view paste」というリンクをクリックするときちんとインデントされます。 後は対話的インタプリタの評価のところでLingrと通信しているせいで反応が遅くなっていますが… それは通信部分だけ別スレッドで動かすなどの解決方法がありそうです。

import idlelib

from lingr import *
lingr = Lingr("***YOUR LINGR API KEY***")
lingr.api.session.create()
r = lingr.api.room
r.enter(id="***id***", nickname="***YOUR NICKNAME***", password="***YOUR PASSWORD***")

LOCALS = {}

class Tee(object):
	def __init__(self, stdout):
		self.buf = []
		self.stdout = stdout
	def write(self, s):
		self.buf.append(s)
		self.stdout.write(s)
		
import codeop
CC = codeop.CommandCompiler()

def wrap(f):
	def foo(self, s):
		result = f(self, s)
		if result == False:
			r.say(message = ">>> " + s + "\n\n")
			import sys
			sys.stdout = Tee(sys.stdout)
			exec CC(s, "single") in LOCALS
			r.say(message = "".join(sys.stdout.buf) + "\n" )
			sys.stdout = sys.stdout.stdout
		return result
	return foo

MI = idlelib.PyShell.ModifiedInterpreter
MI.runsource = wrap(MI.runsource)
idlelib.PyShell.main()

追記:r.enter(id="***id***", nickname="***YOUR NICKNAME***", password="***YOUR PASSWORD***")のnicknameとpasswordはあなたのアカウントのものではなく、「ボットが名乗るてきとうなニックネーム」と「チャットルームのパスワード」です。テスト用にパスワードでアクセス制限をしたチャットルームを使っていたのでパスワードを指定していますが、普通のチャットルームなら必要ありません。

あとCommandCompilerを使わなくてもcompile(source, "<string>", "single")でよさそうです。

2007年06月05日

PyCodeObjectを書き換える

Pythonのcodeオブジェクトを書き換えてしまう拡張ライブラリ「CodeHack」を作りました。 下の使用例では、関数の中の加算命令を乗算命令に書き換えています。
import CodeHack

def f(x):
        print x + 2

f(10) #-> 12

# replace ADD to MULTIPLY
code = f.func_code.co_code
code = code.replace("\x17", "\x14")
CodeHack.set_co_code(f.func_code, code)

f(10) #-> 20 

Pythonの拡張ライブラリを作って公開するのも初めてですし、 C言語をさわるのも数年ぶりなのですが、 想像以上に簡単でした。 全部distutilsがやってくれるので、 コンパイラを自分で呼ぶ必要すらありませんでした。 see Python モジュールの配布

ダウンロード

(22:31にバージョン0.02を公開しました。0.01はたまにPythonが異常終了します。)

CodeHack-0.0.2.win32.exe(Windows用インストーラ)

CodeHack-0.02.zip(ソースコード)

原理

原理はとても簡単(原理と言うほどでもないくらい簡単)です。 コードオブジェクトのco_code属性はリードオンリーフラグが立っているためにPythonのプログラムで直接書き換えることはできませんが、 Cのコードから書き換えることは可能です。 そこで、コードオブジェクトと文字列を受け取って、co_codeを書き換えてしまうモジュールをCで書きました。
#include <Python.h>

static PyObject *
codehack(PyObject *self, PyObject *args)
{
  PyObject* c; // PyCodeObject
  PyObject* s; // PyString

  if (!PyArg_ParseTuple(args, "OO", &c, &s))
	return NULL;
  PyCodeObject* code = (PyCodeObject*) c;
  Py_INCREF(s);
  code->co_code = s;
  Py_RETURN_NONE;
}

static PyMethodDef Methods[] = {
  {"set_co_code", codehack, METH_VARARGS, "overwrite co_code"},
  {NULL, NULL, 0, NULL}
};

PyMODINIT_FUNC
initCodeHack(void)
{
  (void) Py_InitModule("CodeHack", Methods);
}

Python拡張の作り方

いくつかの落とし穴をよければ意外と簡単だったので、 この文章を必要としている誰かのために整理してみることにします。

追記: おびなたのはてな日記 - distutils with boost.pythonで詳しく説明されているように、C++とBoostを使うともっと楽になるようです。近いうちに試してみます。 また、 Cybozu Developer Network: Python調査報告 (2006/10) によれば 「PEAKの setuptools は distutils の拡張で(中略)distutils の上位互換であるため、setuptools を使用しない理由はありません。積極的に使用しましょう。」とのことなのでこちらも試してみたいと思います。

Cのコードを用意する

書き換えるべき所を明確にするために、 [[MODULE_NAME]]というように表記しました。 同じ名前の括弧には同じ文字列が入らなければいけません。 ***と書いてある部分には、文脈にあわせて適切なことを書く必要があります。
#include <Python.h>

static PyObject *
[[FUNC_NAME]](PyObject *self, PyObject *args)
{
  ***; // 変数の宣言

  if (!PyArg_ParseTuple(args, ***)) //受け取る引数にあわせて書き換える
        return NULL;

  ***; // 肝心の処理

  Py_RETURN_NONE; //値を返さないでいい場合
  return Py_BuildValue(***); //返す場合
}

static PyMethodDef Methods[] = {
  {"[[PYTHON_FUNC_NAME]]", [[FUNC_NAME]], METH_VARARGS, "[[関数の説明]]"},
  {NULL, NULL, 0, NULL}
};

PyMODINIT_FUNC
init[[MODULE_NAME]](void)
{
  (void) Py_InitModule("[[MODULE_NAME]]", Methods);
} 

setup.pyを作る

from distutils.core import setup, Extension

module1 = Extension(
	'[[PACKAGE_NAME]].[[MODULE_NAME]]',
	sources = ['[[C_FILE_NAME]].c']
)

setup(
	name = '[[PACKAGE_NAME]]',
	version = '[[バージョン]]',
	description = '[[説明文]]',
	url = '[[URL]]',
	author = '[[NAME]]',
	author_email = '[[E-MAIL]]',
	ext_modules = [module1],
	packages=["[[PACKAGE_NAME]]"],
)
PACKAGE_NAMEはPYTHON_MODULE_NAMEと同じでもかまいません。 Extensionの引数に「A.B」と指定した場合、 Aというフォルダの中にB.pydが生成されます。

パッケージを作る

[[PACKAGE_NAME]]と同じ名前のフォルダを作り、 そこに__init__.pyやテスト用のスクリプトなどを置きます。 この位置に置いたスクリプトは[[MODULE_NAME]].pydと同じフォルダにコピーされるので import [[MODULE_NAME]]でインポートすることができます。

パッケージを作るようにsetup.pyを記述したので、 ここで__init__.pyを作成して最低限下のように書いておかないと この後のリリースの時に困ります。

import [[MODULE_NAME]]

__all__ = ["[[MODULE_NAME]]"]
__init__.pyを作る手間が惜しければパッケージを指定しないで、 作成されたバイナリを手でパスの通ったところにコピーしても構いませんが、 あとで余計に手間がかかるかもしれません。

ビルド

コマンドプロンプトを立ち上げてsetup.pyのあるフォルダへ移動します。 基本的には「python setup.py build」でdistutilsがよしなに取りはからってくれます。 筆者の環境では「Cygwinでビルドするなら-c mingw32とつけろ」とメッセージが出るので 「python setup.py build -c mingw32」としました。

Cのコードに問題がある場合はここで表示されます。

テスト

buildフォルダの中(たとえばbuild\lib.win32-2.5\[[PACKAGE_NAME]])に コンパイルされたバイナリと、その他のスクリプトがコピーされているはずです。 テスト用のスクリプトを起動して動作確認をします。

コンパイルされたモジュールを一度インポートすると、 そのインタプリタが生きている間は上書きに失敗するためコンパイルができません。 delでモジュールを消しても同様です。 Cのコードを書き換えてビルドし直す場合には忘れずにインタプリタを終了しましょう。

インストーラの作成

「python setup.py bdist_wininst」でウィンドウズ版のインストーラが作成されます。 しかし筆者の環境では「-c mingw32をつけろ」というエラーメッセージがが表示され、 言われたとおりにしてもエラーになります。 「python setup.py bdist_wininst --skip-build」 とやってビルドのプロセスを飛ばすのが正解です。

ソースリリースの作成

Windows用のインストーラだけでは他のOSを使っている人が困るので、 ソースリリースも作成します。 「python setup.py sdist」でOK、のはずですがなぜか パッケージの中身の__init__.pyやtest.pyがコピーされなかったので手でコピーしました。 Windows用インストーラの方はきちんとコピーされているので謎です。

まとめ

何年か前にPythonの拡張ライブラリを作ろうとしたときには gccでにどういうコンパイルオプションをつければPython.hへのパスが通るのか… などと悩んだのですが、 distutilsを使うことでこんなに簡単にインストーラまでできてしまうことに驚きました。 ぜひ一度試してみることをおすすめします。

2007年06月06日

PythonのバイトコードをGRINEditで可視化

pybc_vis5.PNG

上の図は下のPythonで書かれた関数のバイトコードをGRINEditを使って可視化したものです。濃い青の矢印は「矢印先のインストラクションがスタックに積んだ値を矢印根本のインストラクションが消費した」という意味です。薄い水色の矢印はもうちょっと弱い「先が積んだ値を消費したわけじゃないけど、先がさわったスタックを根本もさわったか見た」という関係です。 赤矢印はジャンプです。

JUMP_IF_FALSEでの分岐で、条件式をポップせずに分岐して、分岐先の両方でいきなりポップしているのはなにか深遠な理由があるのでしょうかね?「JUMP_IF_FALSEは条件式をポップする」という仕様にすればelse節が省略されたときにポップするために4バイト使わないで済むのに。

def facto(n, ans=1):
    if n == 0:
        return 1
    return facto(n - 1, n * ans)
バイトコードは下のようになります。
    0 7C_00_00 LOAD_FAST(n)
    3 64_01_00 LOAD_CONST(0)
    6 6A_02_00 COMPARE_OP(==)
    9 6F_08_00 JUMP_IF_FALSE() -> POP_TOP@20
   12       01 POP_TOP()
   13 64_02_00 LOAD_CONST(1)
   16       53 RETURN_VALUE()
   17 6E_01_00 JUMP_FORWARD() -> LOAD_GLOBAL@21
   20       01 POP_TOP()
   21 74_00_00 LOAD_GLOBAL(facto)
   24 7C_00_00 LOAD_FAST(n)
   27 64_02_00 LOAD_CONST(1)
   30       18 BINARY_SUBTRACT()
   31 7C_00_00 LOAD_FAST(n)
   34 7C_01_00 LOAD_FAST(ans)
   37       14 BINARY_MULTIPLY()
   38 83_02_00 CALL_FUNCTION()
   41       53 RETURN_VALUE() 

いろいろ見ているとハッシュリテラルの作成に大量のROT_TWOが入っていたりとおもしろいです。どう考えてもSTORE_SUBSCRの仕様が間違っているようにしか思えません。

pybc_vis6.PNG

def foo():
    {"a": 1, "b": 2, (1, 2): (1, 2)}
    0 68_00_00 BUILD_MAP() 
    3       04 DUP_TOP() 
    4 64_01_00 LOAD_CONST(1) 
    7       02 ROT_TWO() 
    8 64_02_00 LOAD_CONST(a) 
   11       3C STORE_SUBSCR() 
   12       04 DUP_TOP() 
   13 64_03_00 LOAD_CONST(2) 
   16       02 ROT_TWO() 
   17 64_04_00 LOAD_CONST(b) 
   20       3C STORE_SUBSCR() 
   21       04 DUP_TOP() 
   22 64_05_00 LOAD_CONST((1, 2)) 
   25       02 ROT_TWO() 
   26 64_06_00 LOAD_CONST((1, 2)) 
   29       3C STORE_SUBSCR() 
   30       01 POP_TOP() 
   31 64_00_00 LOAD_CONST(None) 
   34       53 RETURN_VALUE() 

でもGRINEditの最新版をさっさと公開すべきですね、すみません。 GRINEditのドキュメントも書かないといけないですね。 このディスアセンブルや可視化のためのコードは先日PyCodeObjectを書き換えるで紹介したCodeHackライブラリに入れました。そのうちにバージョン0.03で公開したいと思います。

2007年06月12日

Pythonコード添削道場

来る6月30日、東大駒場キャンパスでPython Workshop the Edge 2007が開催されます。

「Pythonコード添削道場」はこのワークショップのセッションの一つで、私、西尾 泰和増田 泰さん柴田 淳さんの3人で投稿されたコードを添削するという企画です。

この企画はみなさんにコードを投稿して頂かないと始まりません。 コードの内容は自由なので、ぜひご投稿ください。

また「投稿してといわれても、ちょうどいいコードがないなぁ」 という人のために、お題が7つ用意してあります。 お題に挑戦してみて結果のコードを投稿するのもよいでしょう。

コードの投稿の際に名前を書く必要はありません。 完全に匿名でも、自分にしかわからないような ハンドルネームでもOKです。 その方が添削する我々も、容赦なく添削ができます。

自分のコードが容赦なく添削される機会は、 そうそうあるもんじゃないですよ! ぜひこのチャンスをものにして 理解を深めるきっかけにしてください。

お題は以下の7問です。

  • お題1:ファイルの同期
  • お題2:単語数カウント
  • お題3:シングルトン
  • お題4:入れ子リストの中身を順に表示
  • お題5:行列の回転
  • お題6:名簿の並び替え
  • お題7:整数とビット列の相互変換
詳細はこちらからどうぞ→Pythonコード添削道場

About 2007年06月

2007年06月にブログ「西尾泰和のブログ @ Cybozu Labs」に投稿されたすべてのエントリーです。過去のものから新しいものへ順番に並んでいます。

前のアーカイブは2007年05月です。

次のアーカイブは2007年08月です。

他にも多くのエントリーがあります。メインページアーカイブページも見てください。