はじめに
このブログは、eeic2022の3年後期実験、大規模ソフトウェアの第5班の成果報告のブログである。
今回は、c言語を用いてpythonにunless文(if文の逆の機能)とuntil文(while文と逆の機能)を追加してみた。また、print関数をc言語のprintf関数のように、自動で改行したり空白を開けたりする機能を失わせてみた。そして、元のprint関数の機能を持つprintln関数を追加してみた。
pythonの文法がどのように構築されているかを知る
いつもpyhtonを使ってるけど、実際に文法がどういう仕組みで成り立っているのかは知らないことが多いですよね?そこで、ソフトウェア初心者がpythonの構造を理解するために、新しい文法unless文、until文の追加を試みました(機能はif文、while文と同じだけど、仕組みを理解するためだけなので許して)。
print関数が使いにくい
pythonのprint関数って便利なようで便利じゃないかも?
例えば、以下のように自動的に改行されてしまう。
print("大規模")
print("ソフトウェア")
>>>大規模
ソフトウェア
もし改行したくないのであれば、
print('大規模', end='')
print('ソフトウェア', end='')
>>>大規模ソフトウェア
と書かなければならないので、ちょっと面倒。
また、2変数を出力する場合に、以下のように自動で空白を開けてしまう。
word1="大規模"
word2="ソフトウェア"
print(word1,word2)
>>>大規模 ソフトウェア
もし空白を開けたくないのであれば、
word1="大規模"
word2="ソフトウェア"
print(word1,word2,sep='')
>>>大規模ソフトウェア
と書かなければならず、これもまた面倒。
ということで、これらの自動改行、自動空白機能を消去できたらもっと使いやすくなりそう!
でも、元のprintの機能もないと困ってしまうので、println関数という新しい関数として元のprint関数を残すことにしました。
事前準備
今回はバージョン管理にGitHubを用いることにしました。
(GitHubを用いたバージョン管理についてはこちらに大変参考になるサイトがあるので見てみてください。
https://eeic-software1.github.io/2021/git/)
まずGitHub上で公開されているCPythonのソースコードをダウンロードします。
次に先程ダウンロードしたファイルを解凍します。
$ tar xvf ダウンロードしたファイル名
解凍したフォルダー内に移動した後以下のコードでビルドしインストールします。./configureを実行する際には--prefix(最終的にプログラムをインストールする場所を指定する。)、-g(デバッガがプログラムの実行中の位置をソースコード上に表示してくれる。)、-O0(プログラムの実行順序がソースコード上の見た目とほぼ同じになる。)のオプションをつけると良いです。後者2つはデバッガでプログラムの動作を追跡する際に非常に役立ちます。
$ CFLAGS="-O0 -g" ./configure --prefix=インストール先のパス
$ make
$ make install
以上でpythonのインストールは完了です。
インストール先にディレクトリを移して以下のコードを実行するとpythonの起動が確認できます。
Python 3.12.0a0 (heads/main:d4940a593f, Oct 24 2022, 16:00:40) [GCC 9.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>
ビルドの際にエラーが出てしまう場合は、もしかしたらpythonをインストールするのに必要なパッケージがPCに入っていないことが原因かもしれません。こちらのサイトを参考に必要なパッケージをインストールしてみてください。
unless文、until文の実装
文法構造の解析方法
while文やif文を含むサンプルプログラムを実行し、端末でデバッガを用いてプログラムに潜って行ってもいいのだが、cpythonは多数のファイルから構成されているので、目的のプログラムを見つけるには結構大変。
実は、Python Developer's Guideというpythonの説明書のようなちょうどいいサイトがあって、そこのChanging CPython’s Grammarに具体的な文法追加方法が書かれている。
変更を加えた箇所
・Grammar/Python.gram
・Parser/Python.asdl
・Python/ast.c
・Python/symtable.c
・Python/compile.c
具体的な変更内容
Grammar/Python.gram:文法の定義に関わる部分。これを変更した後は、make regen-pegenを行うことで変更を正しく反映できる。
Parser/Python.asdl:これを変更した後は、make regen-astを行うことで関連ファイルにも変更をくわえる。
Python/ast.c:文法変更に関わるASTオブジェクトを有効化するために変更する。
ここで、make regen-ast、make regen-pegenを行うと、make regen-pegenを行った段階で以下のようなエラーメッセージが吐かれた。
pegen.grammar.GrammarError: Dangling reference to rule 'unless_stmt'pegen.grammar.GrammarError: Dangling reference to rule 'unless_stmt'
実は375行目を見ていただければわかるとおりに私達はUnless_stmtと1文字目を大文字にしてしまっていた!このせいで題名が間違っているので、タイトルがないところに中身をくわえるというDangling reference(懸垂参照)というエラーが生じていたのだった。小文字に変更。。。
するとうまくいった。
Python/symtable.c:記号表を変更する。
Python/compile.c:コンパイラの変更を行う。
ここにあたって、compiler_jump_if関数が怪しいと感じた。この関数の4引数目の0,1が真偽値となっており、それを変更することによってif,whileの逆の機能を示すunless,until文を実装できると考えた。
unless,untilを使用した際に、compiler_unless,compiler_untilの結果を返り値で返すように、記述を追加した。
すると以下のような結果を示した。
i=0
if i==0:
print("Hello")
unless i==0:
print("Hello")
>>>Hello
>>>
j=0
until j>3:
print("Hello")
j=j+1
>>>Hello
>>>Hello
>>>Hello
print関数の機能変更
whileやifと違いprintは予約語ではないので、Grammar/python.gramにprintの項目は存在しない。printに関係するファイルの説明がないかネットサーフィンしたところ、このサイトを見つけた。
このサイトによると、print関数はPython/bltinmodule.cにbuiltin_print関数として定義されているらしい。実際にbltinmodule.c内で検索をかけてみるとbuiltin_print関数はなかったが、以下のようなコメントアウトが出てくる。
/*[clinic input]
print as builtin_print
*args: object
sep: object(c_default="Py_None") = ' '
string inserted between values, default a space.
end: object(c_default="Py_None") = '\n'
string appended after the last value, default a newline.
file: object = None
a file-like object (stream); defaults to the current sys.stdout.
flush: bool = False
whether to forcibly flush the stream.
Prints the values to a stream, or to sys.stdout by default.
[clinic start generated code]*/
"print as builtin_print"と書いてあるので、この下がbuiltin_print関数に相当すると考えられる。
print関数の自動改行、自動空白機能の削除
上記のコメントアウトを見ると、sepが値の間に挿入される文字列であり、デフォルトで' '(スペース)になることがわかる。これが、自動空白機能に相当していると推測される。また、endが最後の値の後ろに現れる文字列であり、デフォルトで'\n'(改行)になっていることがわかる。これが、自動改行機能に相当していると推測される。
コメントアウト以下には、builtin_println_impl関数が定義されており、その中で該当部分を探してみた。
static PyObject *
builtin_print_impl(PyObject *module, PyObject *args, PyObject *sep,
PyObject *end, PyObject *file, int flush)
/*[clinic end generated code: output=3cfc0940f5bc237b input=c143c575d24fe665]*/
{
int i, err;
if (file == Py_None) {
PyThreadState *tstate = _PyThreadState_GET();
file = _PySys_GetAttr(tstate, &_Py_ID(stdout));
if (file == NULL) {
PyErr_SetString(PyExc_RuntimeError, "lost sys.stdout");
return NULL;
}
/* sys.stdout may be None when FILE* stdout isn't connected */
if (file == Py_None) {
Py_RETURN_NONE;
}
}
if (sep == Py_None) {
sep = NULL;
}
else if (sep && !PyUnicode_Check(sep)) {
PyErr_Format(PyExc_TypeError,
"sep must be None or a string, not %.200s",
Py_TYPE(sep)->tp_name);
return NULL;
}
if (end == Py_None) {
end = NULL;
}
else if (end && !PyUnicode_Check(end)) {
PyErr_Format(PyExc_TypeError,
"end must be None or a string, not %.200s",
Py_TYPE(end)->tp_name);
return NULL;
}
for (i = 0; i < PyTuple_GET_SIZE(args); i++) {
if (i > 0) {
if (sep == NULL) {
err = PyFile_WriteString(" ", file);
}
else {
err = PyFile_WriteObject(sep, file, Py_PRINT_RAW);
}
if (err) {
return NULL;
}
}
err = PyFile_WriteObject(PyTuple_GET_ITEM(args, i), file, Py_PRINT_RAW);
if (err) {
return NULL;
}
}
if (end == NULL) {
err = PyFile_WriteString("\n", file);
}
else {
err = PyFile_WriteObject(end, file, Py_PRINT_RAW);
}
if (err) {
return NULL;
}
if (flush) {
PyObject *tmp = PyObject_CallMethodNoArgs(file, &_Py_ID(flush));
if (tmp == NULL) {
return NULL;
}
Py_DECREF(tmp);
}
Py_RETURN_NONE;
}
コメントアウトより、デフォルトの時sep,endがPy_Noneであることを考え読み進めると、以下のerr = PyFile_WriteString(" ", file)とerr = PyFile_WriteString("\n", file)が自動空白機能と自動改行の部分であると考えられる。
for (i = 0; i < PyTuple_GET_SIZE(args); i++) {
if (i > 0) {
if (sep == NULL) {
err = PyFile_WriteString(" ", file);
}
...
}
...
}
if (end == NULL) {
err = PyFile_WriteString("\n", file);
}
そこで、これらを以下のように変更。
for (i = 0; i < PyTuple_GET_SIZE(args); i++) {
if (i > 0) {
if (sep == NULL) {
//change_ex
//err = PyFile_WriteString(" ", file);
err = PyFile_WriteString("", file);
//change_ex
}
...
}
...
if (end == NULL) {
//change_ex
//err = PyFile_WriteString("\n", file);
err = PyFile_WriteString("", file);
//change_ex
}
以上をもって、以下のプログラムを実行すると、改行と空白の機能がなくなっていることがわかる。
a = "hello"
b = "world"
print(a,b)
print(a,b)
>>>helloworldhelloworld
println関数の追加
まず、println関数用のbuiltin_print_impl関数をbuiltinln_print_impl関数として複製する。
/*[clinic input]
println as builtin_println
*args: object
sep: object(c_default="Py_None") = ' '
string inserted between values, default a space.
end: object(c_default="Py_None") = '\n'
string appended after the last value, default a newline.
file: object = None
a file-like object (stream); defaults to the current sys.stdout.
flush: bool = False
whether to forcibly flush the stream.
Prints the values to a stream, or to sys.stdout by default.
[clinic start generated code]*/
static PyObject *
builtin_println_impl(PyObject *module, PyObject *args, PyObject *sep,
PyObject *end, PyObject *file, int flush)
/*[clinic end generated code: output=3cfc0940f5bc237b input=c143c575d24fe665]*/
{
int i, err;
if (file == Py_None) {
PyThreadState *tstate = _PyThreadState_GET();
file = _PySys_GetAttr(tstate, &_Py_ID(stdout));
if (file == NULL) {
PyErr_SetString(PyExc_RuntimeError, "lost sys.stdout");
return NULL;
}
/* sys.stdout may be None when FILE* stdout isn't connected */
if (file == Py_None) {
Py_RETURN_NONE;
}
}
if (sep == Py_None) {
sep = NULL;
}
else if (sep && !PyUnicode_Check(sep)) {
PyErr_Format(PyExc_TypeError,
"sep must be None or a string, not %.200s",
Py_TYPE(sep)->tp_name);
return NULL;
}
if (end == Py_None) {
end = NULL;
}
else if (end && !PyUnicode_Check(end)) {
PyErr_Format(PyExc_TypeError,
"end must be None or a string, not %.200s",
Py_TYPE(end)->tp_name);
return NULL;
}
for (i = 0; i < PyTuple_GET_SIZE(args); i++) {
if (i > 0) {
if (sep == NULL) {
err = PyFile_WriteString(" ", file);
}
else {
err = PyFile_WriteObject(sep, file, Py_PRINT_RAW);
}
if (err) {
return NULL;
}
}
err = PyFile_WriteObject(PyTuple_GET_ITEM(args, i), file, Py_PRINT_RAW);
if (err) {
return NULL;
}
}
if (end == NULL) {
err = PyFile_WriteString("\n", file);
}
else {
err = PyFile_WriteObject(end, file, Py_PRINT_RAW);
}
if (err) {
return NULL;
}
if (flush) {
PyObject *tmp = PyObject_CallMethodNoArgs(file, &_Py_ID(flush));
if (tmp == NULL) {
return NULL;
}
Py_DECREF(tmp);
}
Py_RETURN_NONE;
}
参考にしたサイトによると、Python/bltinmodule.cのbuiltin_methodsにもprintが登録されているようなので、そこを見てみると以下のようになっている。
static PyMethodDef builtin_methods[] = {
...
BUILTIN_PRINT_METHODDEF
...
};
よって、println用にBUILTIN_PRINT_METHODDEFも登録する。
static PyMethodDef builtin_methods[] = {
...
BUILTIN_PRINTLN_METHODDEF
...
};
ただ、この段階では、BUILTIN_PRINTLN_METHODDEFが定義されておらず、"println"という表記を登録する場所もないので、新たにこれらを登録するファイルを見つける必要がある。そこでヘッダファイルを見てみると、clinic/bltinmodule.c.hという、それっぽいヘッダファイルがある。
#include "clinic/bltinmodule.c.h"
bltinmodule.c.h内で、printの表記を検索して探すと、PyDoc_STRVARによるドキュメントの定義、BUILTIN_PRINT_METHODDEFの定義、builtin_print_impl関数の宣言、builtin_print関数(ここで所望のbuiltin_print関数が見つかる)の定義が発見できる。
PyDoc_STRVAR(builtin_print__doc__,
"print($module, /, *args, sep=\' \', end=\'\\n\', file=None, flush=False)\n"
"--\n"
"\n"
"Prints the values to a stream, or to sys.stdout by default.\n"
"\n"
" sep\n"
" string inserted between values, default a space.\n"
" end\n"
" string appended after the last value, default a newline.\n"
" file\n"
" a file-like object (stream); defaults to the current sys.stdout.\n"
" flush\n"
" whether to forcibly flush the stream.");
#define BUILTIN_PRINT_METHODDEF \
{"print", _PyCFunction_CAST(builtin_print), METH_FASTCALL|METH_KEYWORDS, builtin_print__doc__},
static PyObject *
builtin_print_impl(PyObject *module, PyObject *args, PyObject *sep,
PyObject *end, PyObject *file, int flush);
static PyObject *
builtin_print(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
{
PyObject *return_value = NULL;
#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
#define NUM_KEYWORDS 4
static struct {
PyGC_Head _this_is_not_used;
PyObject_VAR_HEAD
PyObject *ob_item[NUM_KEYWORDS];
} _kwtuple = {
.ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
.ob_item = { &_Py_ID(sep), &_Py_ID(end), &_Py_ID(file), &_Py_ID(flush), },
};
#undef NUM_KEYWORDS
#define KWTUPLE (&_kwtuple.ob_base.ob_base)
#else // !Py_BUILD_CORE
# define KWTUPLE NULL
#endif // !Py_BUILD_CORE
static const char * const _keywords[] = {"sep", "end", "file", "flush", NULL};
static _PyArg_Parser _parser = {
.keywords = _keywords,
.fname = "print",
.kwtuple = KWTUPLE,
};
#undef KWTUPLE
PyObject *argsbuf[5];
Py_ssize_t noptargs = 0 + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 0;
PyObject *__clinic_args = NULL;
PyObject *sep = Py_None;
PyObject *end = Py_None;
PyObject *file = Py_None;
int flush = 0;
args = _PyArg_UnpackKeywordsWithVararg(args, nargs, NULL, kwnames, &_parser, 0, 0, 0, 0, argsbuf);
if (!args) {
goto exit;
}
__clinic_args = args[0];
if (!noptargs) {
goto skip_optional_kwonly;
}
if (args[1]) {
sep = args[1];
if (!--noptargs) {
goto skip_optional_kwonly;
}
}
if (args[2]) {
end = args[2];
if (!--noptargs) {
goto skip_optional_kwonly;
}
}
if (args[3]) {
file = args[3];
if (!--noptargs) {
goto skip_optional_kwonly;
}
}
flush = PyObject_IsTrue(args[4]);
if (flush < 0) {
goto exit;
}
skip_optional_kwonly:
return_value = builtin_print_impl(module, __clinic_args, sep, end, file, flush);
exit:
Py_XDECREF(__clinic_args);
return return_value;
}
これらをprintln関数用に複製し、以下のコードを書き加えた。
PyDoc_STRVAR(builtin_println__doc__,
"println($module, /, *args, sep=\' \', end=\'\\n\', file=None, flush=False)\n"
"--\n"
"\n"
"Prints the values to a stream, or to sys.stdout by default.\n"
"\n"
" sep\n"
" string inserted between values, default a space.\n"
" end\n"
" string appended after the last value, default a newline.\n"
" file\n"
" a file-like object (stream); defaults to the current sys.stdout.\n"
" flush\n"
" whether to forcibly flush the stream.");
#define BUILTIN_PRINTLN_METHODDEF \
{"println", _PyCFunction_CAST(builtin_println), METH_FASTCALL|METH_KEYWORDS, builtin_println__doc__},
static PyObject *
builtin_println_impl(PyObject *module, PyObject *args, PyObject *sep,
PyObject *end, PyObject *file, int flush);
static PyObject *
builtin_println(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
{
PyObject *return_value = NULL;
#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
#define NUM_KEYWORDS 4
static struct {
PyGC_Head _this_is_not_used;
PyObject_VAR_HEAD
PyObject *ob_item[NUM_KEYWORDS];
} _kwtuple = {
.ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
.ob_item = { &_Py_ID(sep), &_Py_ID(end), &_Py_ID(file), &_Py_ID(flush), },
};
#undef NUM_KEYWORDS
#define KWTUPLE (&_kwtuple.ob_base.ob_base)
#else // !Py_BUILD_CORE
# define KWTUPLE NULL
#endif // !Py_BUILD_CORE
static const char * const _keywords[] = {"sep", "end", "file", "flush", NULL};
static _PyArg_Parser _parser = {
.keywords = _keywords,
.fname = "println",
.kwtuple = KWTUPLE,
};
#undef KWTUPLE
PyObject *argsbuf[5];
Py_ssize_t noptargs = 0 + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 0;
PyObject *__clinic_args = NULL;
PyObject *sep = Py_None;
PyObject *end = Py_None;
PyObject *file = Py_None;
int flush = 0;
args = _PyArg_UnpackKeywordsWithVararg(args, nargs, NULL, kwnames, &_parser, 0, 0, 0, 0, argsbuf);
if (!args) {
goto exit;
}
__clinic_args = args[0];
if (!noptargs) {
goto skip_optional_kwonly;
}
if (args[1]) {
sep = args[1];
if (!--noptargs) {
goto skip_optional_kwonly;
}
}
if (args[2]) {
end = args[2];
if (!--noptargs) {
goto skip_optional_kwonly;
}
}
if (args[3]) {
file = args[3];
if (!--noptargs) {
goto skip_optional_kwonly;
}
}
flush = PyObject_IsTrue(args[4]);
if (flush < 0) {
goto exit;
}
skip_optional_kwonly:
return_value = builtin_println_impl(module, __clinic_args, sep, end, file, flush);
exit:
Py_XDECREF(__clinic_args);
return return_value;
}
以上をもって、以下のプログラムを実行すると、printと同様の機能を持ったprintln関数を実装できたとわかる。
a = "hello"
b = "world"
println(a,b)
println(a,b)
>>>hello world
hello world
おわりに
私たちの班が今回の実験を通して得た一番の教訓は 「とりあえず当たりをつけて色々いじってみる」 ことの大切さです。Pythonなどの大規模ソフトウェアではソースコード全体を理解することは不可能です。私たちは上記の改造を行う際、まず初めはネット上の情報などから改造に必要な全体の大まかな流れを掴みました。しかしその後は対象のファイル内でキーワード検索をして怪しそうな関数や記述をとりあえずいじってみたりもしながら課題を進めていきました。
この記事を読んでいる皆さんも、もし改造に詰まってしまった時は当たりをつけて色々いじってみてください。案外うまくいくかもしれません。
参考にしたサイト
Python Developer's Guide
Pythonの公式ドキュメントです。特にChanging CPython’s Grammarは文法の変更に大変役立ちました。
Pythonにunless文を追加する
Pythonを改造してみた unless文を追加してみた
どちらも先輩方が過去にこの課題に取り組んだ際の記事です。これらの記事から文法変更の大まかな流れを理解できました。
Pythonに組み込み関数「メガンテ」を追加する
組み込み関数を変更、追加するにはどうすればよいのかを理解する助けになりました。
今実験のテキスト
デバッガの扱い方、ビルドのやり方など大規模ソフトウェアをいじる上で必要な基礎の基礎の内容が書かれています。