Skip to content

背景

感觉上一篇文章写的不够详细,就开了个头,让人有点云里雾里,所以这篇文章就来详细的分析一下with open() as f:这个语句的执行过程,为什么 with 打开文件不再需要调用 close 关闭文件。

python 中使用 with 操作文件,为什么不需要手动关闭?

如下是一个简单的例子,打开一个文件 text.txt,pass(空语句,为了保持程序的完整性,不做任何事情,仅占位),然后程序结束。

python
with open('test.txt', 'r') as f:
    pass

使用 dis 模块查看字节码

使用 dis 模块,打印输出上面的代码的字节码,字节码如下。类似汇编指令。

bash
python3.10 -m dis a.py

  2           0 LOAD_NAME                0 (open)
              2 LOAD_CONST               0 ('test.txt')
              4 LOAD_CONST               1 ('r')
              6 CALL_FUNCTION            2
              8 SETUP_WITH               9 (to 28)
             10 STORE_NAME               1 (f)

  3          12 POP_BLOCK

  2          14 LOAD_CONST               2 (None)
             16 DUP_TOP
             18 DUP_TOP
             20 CALL_FUNCTION            3
             22 POP_TOP
             24 LOAD_CONST               2 (None)
             26 RETURN_VALUE
        >>   28 WITH_EXCEPT_START
             30 POP_JUMP_IF_TRUE        17 (to 34)
             32 RERAISE                  1
        >>   34 POP_TOP
             36 POP_TOP
             38 POP_TOP
             40 POP_EXCEPT
             42 POP_TOP
             44 LOAD_CONST               2 (None)
             46 RETURN_VALUE

第一个列 2,3 表示行数,第二列表示字节码的偏移量,第三列表示字节码的操作码,第四列表示操作码的参数,第五列表示操作码的参数的含义。 Python3.7 和 Python3.10 的字节码有些不同。

Python3.7 的相关字节码是:SETUP_WITHWITH_CLEANUP_STARTWITH_CLEANUP_FINISH

而 Python3.10 的相关字节码是:SETUP_WITHWITH_EXCEPT_STARTPOP_JUMP_IF_TRUERERAISEPOP_EXCEPT。显然 Python3.10 的字节码更加复杂,也更加灵活。

分析字节码相关的内核源码

打开 Python 内核源码,搜索SETUP_WITH,找到Python/ceval.c文件,如下。

c
    case TARGET(SETUP_WITH): {
        _Py_IDENTIFIER(__enter__);
        _Py_IDENTIFIER(__exit__);
        PyObject *mgr = TOP();   // 获取栈顶元素
        PyObject *enter = special_lookup(tstate, mgr, &PyId___enter__); // 查找__enter__方法
        PyObject *res;
        if (enter == NULL) {
            goto error;
        }
        PyObject *exit = special_lookup(tstate, mgr, &PyId___exit__);   // 查找__exit__方法
        if (exit == NULL) {
            Py_DECREF(enter);
            goto error;
        }
        SET_TOP(exit);  // 将__exit__方法设置为栈顶元素
        Py_DECREF(mgr);
        res = _PyObject_CallNoArg(enter);   // 调用__enter__方法
        Py_DECREF(enter);
        if (res == NULL)
            goto error;
        /* Setup the finally block before pushing the result
            of __enter__ on the stack. */
        PyFrame_BlockSetup(f, SETUP_FINALLY, INSTR_OFFSET() + oparg,
                            STACK_LEVEL()); // 设置 finally 块

        PUSH(res);
        DISPATCH();
    }

SETUP_WITH的作用是:将with语句中的as后面的对象的__enter__方法的返回值压入栈顶,然后设置finally块,最后跳转到WITH_EXCEPT_START

接下来看看WITH_EXCEPT_START的实现,如下:

c
    case TARGET(WITH_EXCEPT_START): {
        /* At the top of the stack are 7 values:
            - (TOP, SECOND, THIRD) = exc_info()
            - (FOURTH, FIFTH, SIXTH) = previous exception for EXCEPT_HANDLER
            - SEVENTH: the context.__exit__ bound method
            We call SEVENTH(TOP, SECOND, THIRD).
            Then we push again the TOP exception and the __exit__
            return value.
        */
        PyObject *exit_func;
        // exit_func 是__exit__方法,参数是:exc 是异常对象,val 是异常值,tb 是异常的 traceback
        PyObject *exc, *val, *tb, *res;     

        exc = TOP();    // 获取栈顶元素
        val = SECOND(); // 获取第二个元素
        tb = THIRD();  // 获取第三个元素
        assert(!Py_IsNone(exc));
        assert(!PyLong_Check(exc));
        exit_func = PEEK(7);    // 获取第 7 个元素
        PyObject *stack[4] = {NULL, exc, val, tb};
        res = PyObject_Vectorcall(exit_func, stack + 1,
                3 | PY_VECTORCALL_ARGUMENTS_OFFSET, NULL);  // 调用__exit__方法
        if (res == NULL)
            goto error; // 调用__exit__方法失败,跳转到 error

        PUSH(res);
        DISPATCH();
    }

POP_JUMP_IF_TRUE指令代码如下,如果栈顶元素为真,则跳转到指定的位置,否则继续执行下一条指令。

c
    case TARGET(POP_JUMP_IF_TRUE): {
        PREDICTED(POP_JUMP_IF_TRUE);
        PyObject *cond = POP();     // 获取栈顶元素,也就是条件
        int err;
        if (Py_IsFalse(cond)) {     // 如果条件为假,则不跳转
            Py_DECREF(cond);
            DISPATCH();
        }
        if (Py_IsTrue(cond)) {      // 如果条件为真,则跳转到指定位置
            Py_DECREF(cond);
            JUMPTO(oparg);          // 跳转到指定位置,oparg 是指定位置的偏移量
            CHECK_EVAL_BREAKER();
            DISPATCH();
        }
        err = PyObject_IsTrue(cond);    // 如果条件不是真也不是假,则调用 PyObject_IsTrue 方法
        Py_DECREF(cond);
        if (err > 0) {
            JUMPTO(oparg);          // 跳转到指定位置,oparg 是指定位置的偏移量
            CHECK_EVAL_BREAKER();   // 检查是否需要中断
        }
        else if (err == 0)
            ;
        else
            goto error;
        DISPATCH();
    }

为什么会出现既不是真,也不是假的情况,这里就不详细介绍了,感兴趣的可以关注我的公众号,后续会有相关文章介绍。相关代码如下:

c
/* Py_False and Py_True are the only two bools in existence.
Don't forget to apply Py_INCREF() when returning either!!! */

/* Don't use these directly */
PyAPI_DATA(struct _longobject) _Py_FalseStruct;
PyAPI_DATA(struct _longobject) _Py_TrueStruct;

/* Use these macros */
#define Py_False ((PyObject *) &_Py_FalseStruct)
#define Py_True ((PyObject *) &_Py_TrueStruct)

// Test if an object is the True singleton, the same as "x is True" in Python.
PyAPI_FUNC(int) Py_IsTrue(PyObject *x);
#define Py_IsTrue(x) Py_Is((x), Py_True)

// Test if an object is the False singleton, the same as "x is False" in Python.
PyAPI_FUNC(int) Py_IsFalse(PyObject *x);
#define Py_IsFalse(x) Py_Is((x), Py_False)

RERAISE指令代码如下,看起来没有那么复杂,其实就是将异常对象,异常值,异常的 traceback 重新设置到系统中,然后跳转到exception_unwind,最后执行POP_EXCEPT

c
  case TARGET(RERAISE): {
        assert(f->f_iblock > 0);
        if (oparg) {
            f->f_lasti = f->f_blockstack[f->f_iblock-1].b_handler;
        }
        PyObject *exc = POP();
        PyObject *val = POP();
        PyObject *tb = POP();
        assert(PyExceptionClass_Check(exc));
        _PyErr_Restore(tstate, exc, val, tb);
        goto exception_unwind;
    }

总结

  1. with语句的实现原理是:先调用__enter__方法,然后执行with语句块,最后调用__exit__方法。
  2. with语句的实现原理是通过SETUP_WITH指令和WITH_EXCEPT_START指令来实现的。
  3. open 函数已经帮我实现了__enter____exit__方法,所以我们可以直接使用with语句来打开文件,作用域结束后,文件会自动关闭。

本人热衷于编程语言底层实现原理。通过一些空闲时间阅读源码,记录自己的所学及心得。你的关注和鼓励是对我持续输出分享的动力,感谢,共同进步。

公众号