🚇

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()が実行されるようにする

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.cPython/ast.cDefer_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

参考文献

https://github.com/python/cpython

株式会社AVILEN

Discussion