読者です 読者をやめる 読者になる 読者になる

開拓馬’s blog

いろいろやる

Pythonを改造してみた プロンプトをうるさくしてみた

前回まではPythonに新たな予約語を追加してきました。Pythonは与えられたコードをASTなどの中間データに変換しながら、最終的には擬似的なバイトコードを生成しceval.cで処理するという方式を取っていることがわかりました。

しかし、この記事では文法関係の話はまったくしません。というのも、Python改造に取り組むきっかけとなった講義でgdbの使い方を習ったのですが、私達はデバッガまったく使わずにここまでの改変をやってきたのです。せっかくなのでデバッガを使ってコードを読み解きたいと思います。

gdbPythonを追う

ビルドまでの経緯はこちら

Emacs上でM-x gud-gdbを入力しデバッガを起動します。 mainブレークポイントを置いて引数を入れずに(つまりインタラクティブモードが起動するはず)runします。 すると、Programs/python.cが開きました。

そのままnextstepを連打していくと、おおよそ、以下のような動きをすることがわかりました。

  1. Modules/main.cに移動し、各種初期化処理を行う
  2. Python/getopt.c内のサブルーチンでオプションを受け取る
  3. main.cに戻り、オプションに従ってフラグを切り替える
  4. 引数からソースコードの名前を取得、存在しない場合、インタラクティブモードを開始するフラグを建てる
  5. Python/pythonrun.cIntaractiveloop()関数に来る
  6. プロンプト">>> "が表示される

上記の流れを見た上で、やることを決めました。

メッセージの書き換え

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.cPyUnicode_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.htime.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

プロンプトがうるさいインタラクティブモード

ビルドして実行してみました。

f:id:pf_siedler:20161106171817p:plain
こんな感じでプロンプトがうるさくなります f:id:pf_siedler:20161106171821p:plain
スパムのスケッチに登場する食堂、メニューを書き足してみようみたいなイメージ f:id:pf_siedler:20161106171825p:plain
もはやスパムとは無関係なネタ、スペイン宗教裁判も

実は乱数の初期化がうまく行っておらず毎回同じ順番でプロンプトが登場したり、Macだとアスキーアートが崩れたり、粗がありますが完成ということで……

*1:おそらくexitやquitのreprあるいはstrの戻り値を"Use ~ to exit"にしているのだと思われる

*2:説明通りに"help"や"license"と入力すると"Type help()"というメッセージとともに関数の使い方が説明される

*3:モンティ・パイソンというコメディグループのネタ。ちなみにPythonの公式ドキュメントにもメタ構文変数としてspamが登場する

*4:stdlib.hの擬似乱数は性能がよくないので使用を控えるべきという意見もあるが、今回は乱数が偏ってもあまり問題にならないので使うことにした