Pythonを改造してみた プロンプトをうるさくしてみた
前回まではPythonに新たな予約語を追加してきました。Pythonは与えられたコードをASTなどの中間データに変換しながら、最終的には擬似的なバイトコードを生成しceval.c
で処理するという方式を取っていることがわかりました。
しかし、この記事では文法関係の話はまったくしません。というのも、Python改造に取り組むきっかけとなった講義でgdbの使い方を習ったのですが、私達はデバッガまったく使わずにここまでの改変をやってきたのです。せっかくなのでデバッガを使ってコードを読み解きたいと思います。
gdbでPythonを追う
ビルドまでの経緯はこちら
Emacs上でM-x gud-gdb
を入力しデバッガを起動します。
main
にブレークポイントを置いて引数を入れずに(つまりインタラクティブモードが起動するはず)run
します。
すると、Programs/python.c
が開きました。
そのままnext
とstep
を連打していくと、おおよそ、以下のような動きをすることがわかりました。
Modules/main.c
に移動し、各種初期化処理を行うPython/getopt.c
内のサブルーチンでオプションを受け取るmain.c
に戻り、オプションに従ってフラグを切り替える- 引数からソースコードの名前を取得、存在しない場合、インタラクティブモードを開始するフラグを建てる
Python/pythonrun.c
のIntaractiveloop()
関数に来る- プロンプト">>> "が表示される
上記の流れを見た上で、やることを決めました。
メッセージの書き換え
Pythonをファイル名を指定せずに実行すると、インタラクティブモードが起動します。インタラクティブモードを終了したい場合は、ctrl+Dを入力するか、exit()
関数、またはquit()
関数を実行する必要があります。この仕様がちょっと不親切で終了しようとしてexit
と入力するとUse exit() or Ctrl-D (i.e. EOF) to exit
と表示され、終了してくれません。 *1
exit
と入力すれば終了できるようにするのは複雑な例外処理を行う必要がありそうなので仕方ないのかもしれません。しかし、終了の仕方を説明していないのも不親切だと私は思います。「消し方が分からない、やめ方が分からない」ソフトはユーザーをイライラされるものだと思います。
そこで、Python起動時に表示されるメッセージに「exit()
やquit()
と入力すると終了できます」という説明文を付け足すことにしました。
サクッと完成
grepで該当部分の起動時のメッセージを検索したところ、main.c
内でそれらしい文字列が#define
されているのを発見。書き換えます。
#define COPYRIGHT \ "Type \"help()\", \"copyright\", \"credits\" or \"license()\" " \ "for more information.\nType \"exit()\" or \"quit()\" or Ctrl-D to exit."
ついでに、"help"、"license"の後ろに括弧を付けました。 *2 ビルドして実行したところ、メッセージが変化しました。
% ~/mypython/bin/python3 Python 3.5.2 (<<略>>) [GCC 4.8.4] on linux Type "help()", "copyright", "credits" or "license()" for more information. Type "exit()" or "quit()" or Ctrl-D to exit. >>>
ほんとにちょっとした変更ですが、多少は使いやすくなったのかな……?
オプションを追加 おもしろ機能
-a
や--hoge
のようなハイフンから始まるオプションはCUIアプリにとって欠かせないものです。
オプションを受け取る部分の処理はgetopt.c
ようなので、ちょっと書き足して新しい予約語を足してみようと思います。
それでは追加するオプションにはどんな機能を付けましょうか。先程と同じ文字列の変更なら簡単にできそうです。プロンプトの">>> "を適当に他の記号に変えてみることにしました。
そんなわけで、Pythonの名前にちなんで--spam
オプションを付けるとプロンプトが">>> "から"SPAM>> "に変わるという機能を作ってみることにしました。 *3
spamオプションの追加
getopt.c
に--spam
オプション用の処理部分を追加します。ハイフン2つ+単語のオプションには--help
、--version
が存在するので、それを真似します。
else if (wcscmp(argv[_PyOS_optind], L"--version") == 0) { ++_PyOS_optind; return 'V'; } #if SPAM_MODE else if (wcscmp(argv[_PyOS_optind], L"--spam") == 0) { ++_PyOS_optind; return 'P'; } #endif
ヘルプ表示、バージョン表示は-h
-V
でもできます。どうやら--help
を受け取った場合、内部で-h
に変換するという方式を取っているようなのでspamにもアルファベット一字のオプションを割り当てることにします。spamの頭文字である-s
、-S
はすでに使われているのでs"p"amの-P
を割り当てました。
ビルドして実行したところ、--spam
オプションを付けても"Unknown option"エラーが発生しませんでした。
% ~/mypython/bin/python3 --hoge #存在しないオプションを付けるとエラーを起こす Unknown option: -- <<略>> % ~/mypython/bin/python3 --spam #普通に起動した Python 3.5.2 (<<略>>) [GCC 4.8.4] on linux <<略>> % ~/mypython/bin/python3 -P #普通に起動した Python 3.5.2 (<<略>>) [GCC 4.8.4] on linux <<略>>
spamモードの追加
main.c
に--spam
オプションを受け取ったときの処理を付け加えます。プロンプトの表示はPython/pythonrun.c
で行われるのでspamフラグが立ったことを伝えなければなりません。とりあえずグローバル領域でフラグ変数を定義して伝えることにしました。
/* command line options */ #if SPAM_MODE #define BASE_OPTS L"bBc:dEhiIJm:OqRsStuvVPW:xX:?" int Active_spam_mode = 0; #else #define BASE_OPTS L"bBc:dEhiIJm:OqRsStuvVW:xX:?" #endif : : while ((c = _PyOS_GetOpt(argc, argv, PROGRAM_OPTS)) != EOF) { if (c == 'c') { : switch(c){ : case 'V': version++; break; case 'P': Active_spam_mode++; break;
pythonrun.c
でPyUnicode_FromString()
関数の引数に">>> "という文字列を与えているのを発見。適当に他の文字列にしてビルドしてみるとプロンプトが変わりました。どうやらこの部分でプロンプトの文字を定義しているようなので、フラグが立っているときだけ"SPAM>> "を与えるように変更します。
#if SPAM_MODE extern int Active_spam_mode;//main.cで定義したグローバル変数をextern char* spam_message()//あとで拡張できるように関数にしておく { return "SPAM>> "; } #endif : : #if SPAM_MODE if(Active_spam_mode) { _PySys_SetObjectId(&PyId_ps1, v = PyUnicode_FromString(spam_message())); } else { _PySys_SetObjectId(&PyId_ps1, v = PyUnicode_FromString(">>> ")); } #else _PySys_SetObjectId(&PyId_ps1, v = PyUnicode_FromString(">>> ")); #endif
実行してみます。
% ~/mypython/bin/python3 Python 3.5.2 (<<略>>) [GCC 4.8.4] on linux <<略>> >>> % ~/mypython/bin/python3 --spam Python 3.5.2 (<<略>>) [GCC 4.8.4] on linux <<略>> SPAM>> print("やったぜ。") やったぜ。 SPAM>>
プロンプトがが変わりました!spamモード実装完了です!
文字列をランダムに表示させる
ただプロンプトが変わるだけでは物足りないので、複数パターンのプロンプトがランダムに表示されるように変更してみます。
どうやら標準ライブラリをインクルードしても問題なくビルドできるようなのでstdlib.h
とtime.h
を用いた一番簡単な乱数生成を使うことにします。 *4
プロンプトのレパートリーは適当に5~6個用意しました。
//time.h stdlib.hのインクルード #if SPAM_MODE #include <stdlib.h> #include <time.h> #endif #if SPAM_MODE extern int Active_spam_mode; char* spam_message() { int c; static int f; if(!f){ f = 1; srand((unsigned int)time(NULL)); } c = (int)random()%10; switch(c){ case 0: case 1: return " _____ _____ __ __ \n / ____| __ \\ /\\ | \\/ |\n| (___ | |__) / \\ | \\ / |\n \\___ \\| ___/ /\\ \\ | |\\/| |\n ____) | | / ____ \\| | | |\n|_____/|_| /_/ \\_\\_| |_>> "; case 2: case 3: case 4: return "spam>> "; case 5: case 6: return "SPAM>> "; case 7: return "\n\n _____ _ _ _____ ___ ________ _ _ _ _ \n|_ _| | | | ___| | \\/ | ___| \\ | | | | |\n | | | |_| | |__ | . . | |__ | \\| | | | |\n | | | _ | __| | |\\/| | __|| . ` | | | |\n | | | | | | |___ | | | | |___| |\\ | |_| |\n \\_/ \\_| |_\\____/ \\_| |_\\____/\\_| \\_/\\___/\n==============================================\n\n1.egg bacon and spam\n2.egg bacon sausage and spam\n3.spam bacon sausage and spam\n4. "; case 8: return "\\(^o^)/SPAM\\(^o^)/SPAM\\(^o^)/>> "; case 9: return "Nobody expects the Spanish Inquisition!\nOur two weapons are fear and surprise...and ruthless efficiency...\nOur THREE weapons are fear, surprise... "; default: return ">>> "; } } #endif
プロンプトがうるさいインタラクティブモード
ビルドして実行してみました。
こんな感じでプロンプトがうるさくなります
スパムのスケッチに登場する食堂、メニューを書き足してみようみたいなイメージ
もはやスパムとは無関係なネタ、スペイン宗教裁判も
実は乱数の初期化がうまく行っておらず毎回同じ順番でプロンプトが登場したり、Macだとアスキーアートが崩れたり、粗がありますが完成ということで……