CPythonにGoのDeferを実装してみよう
CPythonにGoのDeferを実装してみよう
この記事では、CPythonの内部構造を軽く見ていくために、新しい文法を最新CPython(3.14)に実装する。
GoのDeferとは?
-
Goに存在する特別なキーワードであり、
defer
で登録された関数は、その関数のScopeの外に抜け出す際に実行される -
LIFOで、登録された逆順に実行されていく
-
主に、GoではCleanup関数を事前に登録する形で利用する
f, err := os.Open(somefile) if err != nil { return err } defer f.Close() // a lot more code // use f down here // do more stuff return nil
目標
下のPythonコードがCPythonから動くようにする
def cleanup():
print("Cleaning up resources...")
def main():
dictionary = {"key": "value"}
defer cleanup()
defer print(f"Final value: {dictionary['key']}")
print("Start main function")
# ... do some work ...
print("Working...")
dictionary["key"] = "new value"
print("End main function")
if __name__ == "__main__":
main()
# Expected output
# ---------------
# Start main function
# Working...
# End main function
# Final value: new value
# Cleaning up resources...
前提
https://github.com/python/cpython/tree/3.14
現時点(2025/04/22)で内容が確定したReleaseの中で一番最新である3.14.0 beta1に対して実装を行う
やらないこと
-
defer
のPython導入に対するDiscussion - テストの作成など、Production Readyのための作業
- ArmApple以外のPlatformでの動作確認
- など、あくまでDeferを最小限の修正で最小限に動くものを実装することのみを目的とする。
実装する内容
-
defer
というキーワード追加され、defer expr()
のような表現を可能にする -
defer expr()
という表現が*.pyをcompile時に見つかったら、expr()
で作られるOpCodesを現在Scopeの一番最後に持っていく。- よって、Goと同じ順で現在Scopeの最後で
expr()
が実行されるようにする
- よって、Goと同じ順で現在Scopeの最後で
Keywordを認識可能にする
PythonのAST Treeにdefer
を追加し、defer
というキーワードがStatementとして認識できるようにする。
まず、CpythonのParserがdefer
キーワードを認識できるように修正する。
# Parser/Python.asdl
module Python
{
mod = Module(stmt* body, type_ignore* type_ignores)
| Interactive(stmt* body)
| Expression(expr body)
| FunctionType(expr* argtypes, expr returns)
stmt = FunctionDef(identifier name, arguments args,
stmt* body, expr* decorator_list, expr? returns,
string? type_comment, type_param* type_params)
| AsyncFunctionDef(identifier name, arguments args,
stmt* body, expr* decorator_list, expr? returns,
string? type_comment, type_param* type_params)
| ClassDef(identifier name,
expr* bases,
keyword* keywords,
stmt* body,
expr* decorator_list,
type_param* type_params)
| Return(expr? value)
| Delete(expr* targets)
| Assign(expr* targets, expr value, string? type_comment)
| TypeAlias(expr name, type_param* type_params, expr value)
| AugAssign(expr target, operator op, expr value)
-- 'simple' indicates that we annotate simple name without parens
| AnnAssign(expr target, expr annotation, expr? value, int simple)
-- use 'orelse' because else is a keyword in target languages
| For(expr target, expr iter, stmt* body, stmt* orelse, string? type_comment)
| AsyncFor(expr target, expr iter, stmt* body, stmt* orelse, string? type_comment)
| While(expr test, stmt* body, stmt* orelse)
| If(expr test, stmt* body, stmt* orelse)
| With(withitem* items, stmt* body, string? type_comment)
| AsyncWith(withitem* items, stmt* body, string? type_comment)
| Match(expr subject, match_case* cases)
| Raise(expr? exc, expr? cause)
| Try(stmt* body, excepthandler* handlers, stmt* orelse, stmt* finalbody)
| TryStar(stmt* body, excepthandler* handlers, stmt* orelse, stmt* finalbody)
| Assert(expr test, expr? msg)
| Import(alias* names)
| ImportFrom(identifier? module, alias* names, int? level)
| Global(identifier* names)
| Nonlocal(identifier* names)
| Expr(expr value)
+ | Defer(expr deferred)
| Pass | Break | Continue
次に、defer
というNodeに対し、defer
をPython文法として認識できるようにする。
# Grammar/python.gram
simple_stmt[stmt_ty] (memo):
| assignment
| &"type" type_alias
| e=star_expressions { _PyAST_Expr(e, EXTRA) }
| &'return' return_stmt
| &('import' | 'from') import_stmt
| &'raise' raise_stmt
| &'pass' pass_stmt
| &'del' del_stmt
| &'yield' yield_stmt
| &'assert' assert_stmt
| &'break' break_stmt
| &'continue' continue_stmt
| &'global' global_stmt
| &'nonlocal' nonlocal_stmt
+ | &'defer' defer_stmt
...
+ defer_stmt[stmt_ty]:
+ | 'defer' a=expressions &(';' | NEWLINE) { _PyAST_Defer(a, EXTRA) }
最後に実際動かすために、この定義式をCコードとして生成する必要がある
make regen-pegen regen-ast
これで、Parser/parser.c
とPython/ast.c
にDefer_kind
というEnumが生成され、defer
に対するロジックが作成できるようになる。
// Python/ast.c
static int
validate_stmt(stmt_ty stmt)
{
...
+ case Defer_kind:
+ ret = validate_expr(stmt->v.Defer.deferred, Load);
+ break;
...
}
これでmake
コマンドを実行しBuildさせ、最初のPythonコードを実行してみると、文法エラーなく実行ができるようになる。
[20/05,08:42:15]~/Pro…/cpython $ ./python.exe sample.py
Start main function
Working...
End main function
ただし、現在の状態だと、Deferの行はByteCodeに変換されていないため、その行は評価されず、何を入れてもエラーが出ない無法地帯となる。
文法エラーを出す
この無法地帯をなんとかするため、まず、今回はDefer+関数で、最後にその関数をまとめ実行する、というフローであるため、関数以外の入力はいらない。よって、その場合にはエラーを吐き出したい。
// Python/codegen.c
static int
codegen_defer(compiler *c, stmt_ty s)
{
location loc = LOC(s);
if (s->v.Defer.deferred->kind != Call_kind) {
return _PyCompile_Error(c, loc, "defer statement must be a call");
}
return SUCCESS;
}
...
static int
codegen_visit_stmt(compiler *c, stmt_ty s)
{
switch (s->kind) {
...
case Defer_kind:
return codegen_defer(c, s);
...
}
これで、defer
の次に関数以外を入れた場合、Compileエラーを吐かせることができる。
[20/05,08:49:00]~/Pro…/cpython $ ./sample.py
File "/Users/jasonyun/Projects/cpython/./sample.py", line 9
defer cleanup
^^^^^^^^^^^^^
SyntaxError: defer statement must be a call
これで、defer
は必要とする関数のみを受け入れるようになった。
内部ロジック実装
CPythonにはScope毎にCompile情報を持つPython/compile.c:compiler_unit
というStructが存在するので、これを利用する。
// Python/compile.c
/* The following items change on entry and exit of code blocks.
They must be saved and restored when returning to a block.
*/
struct compiler_unit {
PySTEntryObject *u_ste;
int u_scope_type;
PyObject *u_private; /* for private name mangling */
PyObject *u_static_attributes; /* for class: attributes accessed via self.X */
PyObject *u_deferred_annotations; /* AnnAssign nodes deferred to the end of compilation */
PyObject *u_conditional_annotation_indices; /* indices of annotations that are conditionally executed (or -1 for unconditional annotations) */
long u_next_conditional_annotation_index; /* index of the next conditional annotation */
instr_sequence *u_instr_sequence; /* codegen output */
instr_sequence *u_stashed_instr_sequence; /* temporarily stashed parent instruction sequence */
int u_nfblocks;
int u_in_inlined_comp;
int u_in_conditional_block;
_PyCompile_FBlockInfo u_fblock[CO_MAXBLOCKS];
_PyCompile_CodeUnitMetadata u_metadata;
+ expr_ty *deferred_calls;
+ Py_ssize_t deferred_count;
+ Py_ssize_t deferred_alloc;
};
deferred_callsにdefer
で登録された関数を保存し、Scopeから抜け出す際に実行すればいけそう。
このObjectを利用するため、初期化やリソース整理などのロジックを追加する
// Python/compile.c
static void
compiler_unit_free(struct compiler_unit *u)
{
Py_CLEAR(u->u_instr_sequence);
Py_CLEAR(u->u_stashed_instr_sequence);
Py_CLEAR(u->u_ste);
Py_CLEAR(u->u_metadata.u_name);
Py_CLEAR(u->u_metadata.u_qualname);
Py_CLEAR(u->u_metadata.u_consts);
Py_CLEAR(u->u_metadata.u_names);
Py_CLEAR(u->u_metadata.u_varnames);
Py_CLEAR(u->u_metadata.u_freevars);
Py_CLEAR(u->u_metadata.u_cellvars);
Py_CLEAR(u->u_metadata.u_fasthidden);
Py_CLEAR(u->u_private);
Py_CLEAR(u->u_static_attributes);
Py_CLEAR(u->u_deferred_annotations);
Py_CLEAR(u->u_conditional_annotation_indices);
+ PyMem_Free(u->deferred_calls);
PyMem_Free(u);
}
...
int
_PyCompile_EnterScope(compiler *c, identifier name, int scope_type,
void *key, int lineno, PyObject *private,
_PyCompile_CodeUnitMetadata *umd)
{
struct compiler_unit *u;
u = (struct compiler_unit *)PyMem_Calloc(1, sizeof(struct compiler_unit));
...
+ u->deferred_calls = NULL;
+ u->deferred_count = 0;
+ u->deferred_alloc = 0;
...
return SUCCESS;
}
u→*
のPropertyはこの後に実装するcodege.c
からではアクセスが不可能になっているため、それ以外のdeferred_*
を編集するための関数を用意し、codegen.c
から使えるようにHeaderに追加する。
// Python/compile.c
int
_PyCompile_PushDeferredCall(compiler *c, expr_ty e)
{
// 新規ExpressionをDeferred BufferにPushしておく。
struct compiler_unit *u = c->u;
if (u->deferred_count >= u->deferred_alloc) {
Py_ssize_t new_alloc = u->deferred_alloc ? u->deferred_alloc * 2 : 4;
expr_ty *new_buf = PyMem_Realloc(u->deferred_calls,
new_alloc * sizeof(expr_ty));
if (!new_buf) {
PyErr_NoMemory();
return -1;
}
u->deferred_calls = new_buf;
u->deferred_alloc = new_alloc;
}
u->deferred_calls[u->deferred_count++] = e;
return 0;
}
Py_ssize_t
_PyCompile_NumDeferredCalls(compiler *c)
{
return c->u->deferred_count;
}
expr_ty
_PyCompile_GetDeferredCall(compiler *c, Py_ssize_t i)
{
return c->u->deferred_calls[i];
}
void
_PyCompile_ClearDeferredCalls(compiler *c)
{
c->u->deferred_count = 0;
}
// Include/Internal/pycore_compile.h
...
int _PyCompile_PushDeferredCall(struct _PyCompiler *c, expr_ty e);
Py_ssize_t _PyCompile_NumDeferredCalls(struct _PyCompiler *c);
expr_ty _PyCompile_GetDeferredCall(struct _PyCompiler *c, Py_ssize_t i);
void _PyCompile_ClearDeferredCalls(struct _PyCompiler *c);
...
そして、これらの関数を利用し、defer
がソースコード上に見つかったらDeferBuffer
にExpressionを入れ、Scopeから抜け出す際に溜まっているExpressionを順次実行させるコードを作成する。
// Python/codegen.c
+static int
+codegen_visit_defer(compiler *c)
+{
+ Py_ssize_t n = _PyCompile_NumDeferredCalls(c);
+ for (Py_ssize_t i = n - 1; i >= 0; --i) {
+ expr_ty e = _PyCompile_GetDeferredCall(c, i);
+ VISIT(c, expr, e);
+ }
+ _PyCompile_ClearDeferredCalls(c);
+ return SUCCESS;
+}
...
static int
codegen_function_body(compiler *c, stmt_ty s, int is_async, Py_ssize_t funcflags,
int firstlineno)
{
...
bool add_stopiteration_handler = ste->ste_coroutine || ste->ste_generator;
if (add_stopiteration_handler) {
/* codegen_wrap_in_stopiteration_handler will push a block, so we need to account for that */
RETURN_IF_ERROR(
_PyCompile_PushFBlock(c, NO_LOCATION, COMPILE_FBLOCK_STOP_ITERATION,
start, NO_LABEL, NULL));
}
for (Py_ssize_t i = first_instr; i < asdl_seq_LEN(body); i++) {
VISIT_IN_SCOPE(c, stmt, (stmt_ty)asdl_seq_GET(body, i));
}
if (add_stopiteration_handler) {
RETURN_IF_ERROR_IN_SCOPE(c, codegen_wrap_in_stopiteration_handler(c));
_PyCompile_PopFBlock(c, COMPILE_FBLOCK_STOP_ITERATION, start);
}
+
+ RETURN_IF_ERROR_IN_SCOPE(c, codegen_visit_defer(c));
+
PyCodeObject *co = _PyCompile_OptimizeAndAssemble(c, 1);
_PyCompile_ExitScope(c);
if (co == NULL) {
Py_XDECREF(co);
return ERROR;
}
int ret = codegen_make_closure(c, LOC(s), co, funcflags);
Py_DECREF(co);
return ret;
}
...
// 上で実装した関数
static int
codegen_defer(compiler *c, stmt_ty s)
{
location loc = LOC(s);
if (s->v.Defer.deferred->kind != Call_kind) {
return _PyCompile_Error(c, loc, "defer statement must be a call");
}
+
+ RETURN_IF_ERROR(_PyCompile_PushDeferredCall(c, s->v.Defer.deferred));
+
return SUCCESS;
}
最後に、symtable.c
を修正し、defer
のStatementから現在のScopeやGlobalの変数や関数を適切に認識できるようにする。
// Python/symtable.c
static int
symtable_visit_stmt(struct symtable *st, stmt_ty s)
{
...
+ case Defer_kind: {
+ if (!symtable_visit_expr(st, s->v.Defer.deferred)) {
+ return 0;
+ }
+ break;
+ }
}
LEAVE_RECURSIVE();
return 1;
}
これで実装は終わり、再度make
でCPythonをBuildし、最初のPythonコードを実行すると、下のような結果が得られる。
[20/05,09:16:59]~/Pro…/cpython $ ./sample.py
Start main function
Working...
End main function
Final value: new value
Cleaning up resources...
そして、dis.dis
を利用し、BytesCodesを確認してみると、下のようになり、Defer関数が適切に登録されていることがわかる。
6 RESUME 0
7 LOAD_CONST 0 ('key')
LOAD_CONST 1 ('value')
BUILD_MAP 1
STORE_FAST 0 (dictionary)
12 LOAD_GLOBAL 1 (print + NULL)
LOAD_CONST 2 ('Start main function')
CALL 1
POP_TOP
16 LOAD_GLOBAL 1 (print + NULL)
LOAD_CONST 3 ('Working...')
CALL 1
POP_TOP
17 LOAD_CONST 4 ('new value')
LOAD_FAST_BORROW 0 (dictionary)
LOAD_CONST 0 ('key')
STORE_SUBSCR
19 LOAD_GLOBAL 1 (print + NULL)
LOAD_CONST 5 ('End main function')
CALL 1
POP_TOP
10 LOAD_GLOBAL 1 (print + NULL)
LOAD_CONST 6 ('Final value: ')
LOAD_FAST_BORROW 0 (dictionary)
LOAD_CONST 0 ('key')
BINARY_OP 26 ([])
FORMAT_SIMPLE
BUILD_STRING 2
CALL 1
9 LOAD_GLOBAL 3 (cleanup + NULL)
CALL 0
LOAD_CONST 7 (None)
RETURN_VALUE
実際の完成版の実装は下のレポジトリを参考
https://github.com/Ja-sonYun/cpython/commit/145a09966d7fe70adc1c30d6a001ca2f4fc367ab
参考文献
Discussion