likes
comments
collection
share

【从1到∞精通Python】5、如何彻底理解with关键字的用法

作者站长头像
站长
· 阅读数 17

原文链接:【Medium Python】最终话:如何彻底理解with关键字的用法?

with关键字的含义,是笔者接触python以来希望彻底搞懂的问题之一,也是一定会困惑大量玩python的同学的问题之一。相信每一个玩过python的同学都接触过with语法,比如with open(xxx) as f的文件操作,或者是with lock这样的加解锁操作,这些东西每个python教程里都有。但是with语法具体表示什么,具体能够翻译成怎样的简单语法,基本没啥人能够说的清楚,说的科学。即便在网上有许多文章在剖析这一点,提到了许多诸如“上下文管理(context manager)”、“异常处理”、“__enter____exit__”之类的词汇,但是就正因为缺少些硬核的东西,比如源码分析,导致许多个文章的内容都很水,看了也不能完完全全的明白,实际写代码的时候也觉得难以彻底掌握。

因此,为了把这件事情说明白,本文决定继续源码分析的套路,让大伙儿彻底理解with关键字是怎么一回事。老样子,一切的吹水都没有源码分析来的实在。看完这篇文章,其它关于with的文章都可以统统无视了。

with代码测试

首先我们上一段测试代码:

import sys
import dis
import threading
from threading import Lock, Thread
import time


def _seg(msg):
    print('\n'.join(['=' * 40, str(msg), '=' * 40]))


def test_fopen():
    filename = '1.log'
    with open(filename) as f:
        print(f.read())
        f.close()
        pass


_NUM_THREADS = 3
_LOCK = Lock()
_CNT = 0
_MAX_CNT = 100


def _test_thread_lock_task():
    thread_name = threading.current_thread().name
    global _CNT
    while True:
        with _LOCK:
            if _CNT < _MAX_CNT:
                _CNT += 1
                print('[%s] count: %d' % (thread_name, _CNT))
            else:
                print('[%s] count reached max: %d' % (thread_name, _MAX_CNT))
                break
            pass


def test_thread_lock():
    sys.setswitchinterval(0.001)
    threads = []
    for i in range(_NUM_THREADS):
        threads.append(Thread(target=_test_thread_lock_task, name='Thread-%d' % (i + 1)))
    for i in range(_NUM_THREADS):
        threads[i].start()
    time.sleep(0)
    for i in range(_NUM_THREADS):
        threads[i].join()


if __name__ == '__main__':
    test_thread_lock()
    test_fopen()
    _seg('disassemble test_fopen')
    dis.dis(test_fopen)
    _seg('disassemble _test_thread_lock_task')
    dis.dis(_test_thread_lock_task)

这段测试代码包含了两个我们常见的with操作:文件读写和线程加锁。test_fopen是读文件内容,test_thread_lock是不同线程交替增加_CNT的操作。在代码里面,再次出现了我们的老同志:反编译库dis,这是为了用来解析每一个函数具体包含哪些操作码,以能够让我们快速定位对应操作的源代码实现。每个被dis的函数,在with的最后都有pass操作,这是为了更加方便看到在退出with范围时,代码实际做了哪些附加操作(嗯,这些pass是实际调试之后才加的)。

以文件读写test_fopen为例,我们看下反编译之后的结果:

 13           0 LOAD_CONST               1 ('1.log')
              2 STORE_FAST               0 (filename)

 14           4 LOAD_GLOBAL              0 (open)
              6 LOAD_FAST                0 (filename)
              8 CALL_FUNCTION            1
             10 SETUP_WITH              36 (to 48)
             12 STORE_FAST               1 (f)

 15          14 LOAD_GLOBAL              1 (print)
             16 LOAD_FAST                1 (f)
             18 LOAD_METHOD              2 (read)
             20 CALL_METHOD              0
             22 CALL_FUNCTION            1
             24 POP_TOP

 16          26 LOAD_FAST                1 (f)
             28 LOAD_METHOD              3 (close)
             30 CALL_METHOD              0
             32 POP_TOP

 18          34 POP_BLOCK
             36 LOAD_CONST               0 (None)
             38 DUP_TOP
             40 DUP_TOP
             42 CALL_FUNCTION            3
             44 POP_TOP
             46 JUMP_FORWARD            16 (to 64)
        >>   48 WITH_EXCEPT_START
             50 POP_JUMP_IF_TRUE        54
             52 RERAISE
        >>   54 POP_TOP
             56 POP_TOP
             58 POP_TOP
             60 POP_EXCEPT
             62 POP_TOP
        >>   64 LOAD_CONST               0 (None)
             66 RETURN_VALUE

我们看到,在with的一行(14),多了SETUP_WITH的操作指令,而在即将退出with代码块的pass一行(18),出现了大量指令,并且有点类似于异常处理的内容。那么这里到底蕴含了什么信息呢? 那么首先,我们从SETUP_WITH——with代码块的初始化操作开始看起。

with代码块的初始化

我们在EvalFrame的循环中,先找到SETUP_WITH对应的代码:

// ceval.c

case TARGET(SETUP_WITH): {
    _Py_IDENTIFIER(__enter__);
    _Py_IDENTIFIER(__exit__);
    PyObject *mgr = TOP();
    PyObject *enter = special_lookup(tstate, mgr, &PyId___enter__);
    PyObject *res;
    if (enter == NULL) {
        goto error;
    }
    PyObject *exit = special_lookup(tstate, mgr, &PyId___exit__);
    if (exit == NULL) {
        Py_DECREF(enter);
        goto error;
    }
    SET_TOP(exit);
    Py_DECREF(mgr);
    res = _PyObject_CallNoArg(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());

    PUSH(res);
    DISPATCH();
}

可以看到,SETUP_WITH操作的步骤如下:

  • 一开始,寻找with对应实例的__enter__以及__exit__方法(绑定实例的),如果两者有其一找不到的话都会直接跳到error报错。
  • 设置__exit__为栈顶
  • 直接调用instance.__enter__()
  • 进行BlockSetup:PyFrame_BlockSetup(f, SETUP_FINALLY, INSTR_OFFSET() + oparg, STACK_LEVEL())
  • __enter__的返回值PUSH到栈上

test_fopen里面,__enter__函数对应的是iobase_enter

// iobase.c

static PyObject *
iobase_enter(PyObject *self, PyObject *args)
{
    if (iobase_check_closed(self))
        return NULL;

    Py_INCREF(self);
    return self;
}

可以看到,在__enter__函数中会检测这个io对象是否已经close掉,如果close掉的话会返回NULL,正常的话返回io对象。如果__enter__返回NULL,在SETUP_WITH里面,就gotoerror逻辑了。

之后我们再看一下BlockSetup语句,其中会调用PyFrame_BlockSetup函数:

// frameobject.c

void
PyFrame_BlockSetup(PyFrameObject *f, int type, int handler, int level)
{
    PyTryBlock *b;
    if (f->f_iblock >= CO_MAXBLOCKS) {
        Py_FatalError("block stack overflow");
    }
    b = &f->f_blockstack[f->f_iblock++];
    b->b_type = type;
    b->b_level = level;
    b->b_handler = handler;
}

PyFrame_BlockSetup的本质是设置了一个PyTryBlock。如果进一步检索PyFrame_BlockSetup的引用的话,会发现SETUP_FINALLY这个操作本质就是调用了这个函数。而SETUP_FINALLY本身,比如在try/except/finally结构里,不论是except还是finally,都用的这个字节码。

PyTryBlock除了在try/except/finally结构中有使用之外,在循环loop的时候也会用到,其三个属性的意义分别为:

  • b_type:当前代码块block的类型(SETUP_FINALLY
  • b_handler:处理错误信息的handler的指令位置(INSTR_OFFSET() + oparg
  • b_level:比如出现exception的场景下,要对栈做恢复,pop一系列栈上的value时,用来参考的栈高度(STACK_LEVEL()

我们进一步看PyTryBlockFrameObject的定义及注释,也可以证实这些信息:

// frameobject.h

typedef struct {
    int b_type;                 /* what kind of block this is */
    int b_handler;              /* where to jump to find handler */
    int b_level;                /* value stack level to pop to */
} PyTryBlock;

struct _frame {
    PyObject_VAR_HEAD
    struct _frame *f_back;      /* previous frame, or NULL */
    PyCodeObject *f_code;       /* code segment */
    PyObject *f_builtins;       /* builtin symbol table (PyDictObject) */
    PyObject *f_globals;        /* global symbol table (PyDictObject) */
    PyObject *f_locals;         /* local symbol table (any mapping) */
    PyObject **f_valuestack;    /* points after the last local */
    /* Next free slot in f_valuestack.  Frame creation sets to f_valuestack.
       Frame evaluation usually NULLs it, but a frame that yields sets it
       to the current stack top. */
    PyObject **f_stacktop;
    PyObject *f_trace;          /* Trace function */
    char f_trace_lines;         /* Emit per-line trace events? */
    char f_trace_opcodes;       /* Emit per-opcode trace events? */

    /* Borrowed reference to a generator, or NULL */
    PyObject *f_gen;

    int f_lasti;                /* Last instruction if called */
    /* Call PyFrame_GetLineNumber() instead of reading this field
       directly.  As of 2.3 f_lineno is only valid when tracing is
       active (i.e. when f_trace is set).  At other times we use
       PyCode_Addr2Line to calculate the line from the current
       bytecode index. */
    int f_lineno;               /* Current line number */
    int f_iblock;               /* index in f_blockstack */
    char f_executing;           /* whether the frame is still executing */
    PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */
    PyObject *f_localsplus[1];  /* locals+stack, dynamically sized */
};

有了PyTryBlock存储一系列栈上信息,就可以保证with结构下的代码块在结束之后,整个栈上的状态能够恢复到with之前的状态。注意这个时候栈顶上是__exit__函数,这样如果之后恢复栈,然后push一系列错误信息,我们的__exit__函数就能处理对应的错误信息了。

BlockSetup之后,就是把__enter__的返回值推进栈里,交由后面的STORE指令存储到locals里面。比如我们在python中编写的with a as b这种形式,最后我们取到的b,就是__enter__的返回值了。

with代码块的退出以及异常处理

执行完with一行的代码之后,我们开始执行with代码块里面的内容。with代码块执行完之后,当退出之时,也会执行一系列行为。 从上面的字节码结果中也可以看到,有非常长的一串,这里也再列出来:

 18          34 POP_BLOCK
             36 LOAD_CONST               0 (None)
             38 DUP_TOP
             40 DUP_TOP
             42 CALL_FUNCTION            3
             44 POP_TOP
             46 JUMP_FORWARD            16 (to 64)
        >>   48 WITH_EXCEPT_START
             50 POP_JUMP_IF_TRUE        54
             52 RERAISE
        >>   54 POP_TOP
             56 POP_TOP
             58 POP_TOP
             60 POP_EXCEPT
             62 POP_TOP
        >>   64 LOAD_CONST               0 (None)
             66 RETURN_VALUE

退出with的一刻,需要考虑两种情况:有异常和没有异常。当没有异常的时候下来,会到字节码的34~46。34先POP_BLOCK退出代码块,然后之后有一个CALL_FUNCTION操作:由于先前讲到栈顶已经被设置成了__exit__函数,那么这里相当于再顶了3个None,然后执行了instance.__exit__(None, None, None)。之后就走到64,退出这个with流程了。 ​

而当有异常时,我们会跳到48:WITH_EXCEPT_START,这一块在前面SETUP_WITH的字节码有标注:

10 SETUP_WITH              36 (to 48)

如果说with结构最终走到了WITH_EXCEPT_START的分支,那么在此之前一定已经执行了某些抛异常(比如raise)且没有捕获的操作。为了模拟这个场景,我们在with代码块中加一行代码raise Exception,来看下抛异常时候的情况。

import dis


def test_with_except():
    with open('./1.log') as f:
        print(f.read())
        raise KeyError('haha')
        pass


if __name__ == '__main__':
    dis.dis(test_with_except)
    test_with_except()

dis得到的反编译结果:

  5           0 LOAD_GLOBAL              0 (open)
              2 LOAD_CONST               1 ('./1.log')
              4 CALL_FUNCTION            1
              6 SETUP_WITH              36 (to 44)
              8 STORE_FAST               0 (f)

  6          10 LOAD_GLOBAL              1 (print)
             12 LOAD_FAST                0 (f)
             14 LOAD_METHOD              2 (read)
             16 CALL_METHOD              0
             18 CALL_FUNCTION            1
             20 POP_TOP

  7          22 LOAD_GLOBAL              3 (KeyError)
             24 LOAD_CONST               2 ('haha')
             26 CALL_FUNCTION            1
             28 RAISE_VARARGS            1

  8          30 POP_BLOCK
             32 LOAD_CONST               0 (None)
             34 DUP_TOP
             36 DUP_TOP
             38 CALL_FUNCTION            3
             40 POP_TOP
             42 JUMP_FORWARD            16 (to 60)
        >>   44 WITH_EXCEPT_START
             46 POP_JUMP_IF_TRUE        50
             48 RERAISE
        >>   50 POP_TOP
             52 POP_TOP
             54 POP_TOP
             56 POP_EXCEPT
             58 POP_TOP
        >>   60 LOAD_CONST               0 (None)
             62 RETURN_VALUE

我们可以从中看到,当raise异常时,会执行RAISE_VARARGS 1的指令。我们先来看RAISE_VARARGS对应的代码:

// ceval.c

case TARGET(RAISE_VARARGS): {
    PyObject *cause = NULL, *exc = NULL;
    switch (oparg) {
        case 2:
            cause = POP(); /* cause */
            /* fall through */
        case 1:
            exc = POP(); /* exc */
            /* fall through */
        case 0:
            if (do_raise(tstate, exc, cause)) {
                goto exception_unwind;
            }
            break;
        default:
            _PyErr_SetString(tstate, PyExc_SystemError,
                             "bad RAISE_VARARGS oparg");
            break;
    }
    goto error;
}

RAISE_VARARGS中,case对应的指令会顺着往下走,直到case 0do_raise逻辑里面。do_raise是抛异常的实际操作,里面会检查抛出的异常类型以及参数是否合理,之后再设置当前线程的异常类型type以及异常值value RAISE_VARARGS最后会跳到error以及exception_unwind代码段:

// ceval.c

error:
        /* Double-check exception status. */
#ifdef NDEBUG
        if (!_PyErr_Occurred(tstate)) {
            _PyErr_SetString(tstate, PyExc_SystemError,
                             "error return without exception set");
        }
#else
        assert(_PyErr_Occurred(tstate));
#endif

        /* Log traceback info. */
        PyTraceBack_Here(f);

        if (tstate->c_tracefunc != NULL)
            call_exc_trace(tstate->c_tracefunc, tstate->c_traceobj,
                           tstate, f);

exception_unwind:

// 暂时忽略下面

error段中,会提取当前frame上的异常traceback信息,然后就直接到了exception_unwind段。exception_unwind段会恢复栈上的信息,其逻辑如下:

// ceval.c

exception_unwind:
        /* Unwind stacks if an exception occurred */
  while (f->f_iblock > 0) {
            /* Pop the current block. */
            PyTryBlock *b = &f->f_blockstack[--f->f_iblock];

            if (b->b_type == EXCEPT_HANDLER) {
                UNWIND_EXCEPT_HANDLER(b);
                continue;
            }
            UNWIND_BLOCK(b);
            if (b->b_type == SETUP_FINALLY) {
                PyObject *exc, *val, *tb;
                int handler = b->b_handler;
                _PyErr_StackItem *exc_info = tstate->exc_info;
                /* Beware, this invalidates all b->b_* fields */
                PyFrame_BlockSetup(f, EXCEPT_HANDLER, -1, STACK_LEVEL());
                PUSH(exc_info->exc_traceback);
                PUSH(exc_info->exc_value);
                if (exc_info->exc_type != NULL) {
                    PUSH(exc_info->exc_type);
                }
                else {
                    Py_INCREF(Py_None);
                    PUSH(Py_None);
                }
                _PyErr_Fetch(tstate, &exc, &val, &tb);
                /* Make the raw exception data
                   available to the handler,
                   so a program can emulate the
                   Python main loop. */
                _PyErr_NormalizeException(tstate, &exc, &val, &tb);
                if (tb != NULL)
                    PyException_SetTraceback(val, tb);
                else
                    PyException_SetTraceback(val, Py_None);
                Py_INCREF(exc);
                exc_info->exc_type = exc;
                Py_INCREF(val);
                exc_info->exc_value = val;
                exc_info->exc_traceback = tb;
                if (tb == NULL)
                    tb = Py_None;
                Py_INCREF(tb);
                PUSH(tb);
                PUSH(val);
                PUSH(exc);
                JUMPTO(handler);
                if (_Py_TracingPossible(ceval2)) {
                    int needs_new_execution_window = (f->f_lasti < instr_lb || f->f_lasti >= instr_ub);
                    int needs_line_update = (f->f_lasti == instr_lb || f->f_lasti < instr_prev);
                    /* Make sure that we trace line after exception if we are in a new execution
                     * window or we don't need a line update and we are not in the first instruction
                     * of the line. */
                    if (needs_new_execution_window || (!needs_line_update && instr_lb > 0)) {
                        instr_prev = INT_MAX;
                    }
                }
                /* Resume normal execution */
                goto main_loop;
            }
        } /* unwind stack */

由于我们先前执行过了PyFrame_BlockSetup(f, SETUP_FINALLY, INSTR_OFFSET() + oparg, STACK_LEVEL()),最终代码会运行到if (b->b_type == SETUP_FINALLY)对应的段落。在其中进行了以下步骤:

  • PyFrame_BlockSetup(f, EXCEPT_HANDLER, -1, STACK_LEVEL()):设定了一个新的代码块,标识为EXCEPT_HANDLER
  • 将异常栈(串连异常信息的链)当前最顶端的异常信息push到栈中
  • 将当前需要raise的异常信息push到栈中
    • 这个场景下,应当和异常栈最顶端的一样
    • 注意_PyErr_Fetch会将表示当前线程要抛出的异常的几个变量(curexc_typecurexc_valuecurexc_traceback)重置为NULL。这样如果当前异常得到妥善处理掉,后面执行时候发现线程里面这些变量是NULL,也不会触发程序终止打印异常。
    • _PyErr_Fetch相反的操作叫做_PyErr_Restore,相当于设定当前线程已经出现异常。

进行了这个操作之后,现在栈上应当至少有7个元素,自顶而下是:

  • 前3个是当前需要raise的异常信息
  • 中间3个是异常栈最顶端的异常信息
  • 然后第7个就是__exit__函数

之后通过JUMPTO(handler)goto main_loop,就走到了WITH_EXCEPT_START逻辑

// ceval.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;
    PyObject *exc, *val, *tb, *res;

    exc = TOP();
    val = SECOND();
    tb = THIRD();
    assert(exc != Py_None);
    assert(!PyLong_Check(exc));
    exit_func = PEEK(7);
    PyObject *stack[4] = {NULL, exc, val, tb};
    res = PyObject_Vectorcall(exit_func, stack + 1,
                              3 | PY_VECTORCALL_ARGUMENTS_OFFSET, NULL);
    if (res == NULL)
        goto error;

    PUSH(res);
    DISPATCH();
}

WITH_EXCEPT_START的逻辑里,直接调用了instance.__exit__(exc_type, exc_value, exc_traceback),然后把结果再推到栈上。这样栈上就有8个元素了。 以先前的with open(xxx) as f为例,其__exit__函数对应了iobase_exit

// iobase.c

static PyObject *
iobase_exit(PyObject *self, PyObject *args)
{
    return PyObject_CallMethodNoArgs(self, _PyIO_str_close);
}

可以看到这个函数会返回f.close的返回值,其实就是None,并且对异常信息(包在args里)没有任何处理。 __exit__函数的返回值有什么用处呢?我们看到紧接着的操作是POP_JUMP_IF_TRUE

// ceval.c

case TARGET(POP_JUMP_IF_TRUE): {
    PREDICTED(POP_JUMP_IF_TRUE);
    PyObject *cond = POP();
    int err;
    if (cond == Py_False) {
        Py_DECREF(cond);
        FAST_DISPATCH();
    }
    if (cond == Py_True) {
        Py_DECREF(cond);
        JUMPTO(oparg);
        FAST_DISPATCH();
    }
    err = PyObject_IsTrue(cond);
    Py_DECREF(cond);
    if (err > 0) {
        JUMPTO(oparg);
    }
    else if (err == 0)
        ;
    else
        goto error;
    DISPATCH();
}

可以看到,一开始我们会POP出来栈顶的值,也就是__exit__的返回值,然后再根据这个返回值走下面的逻辑。如果这个返回值可以作为真值(比如1、有内容的list/dict)的话,就跳到指定的指令,如果不是真值(比如None、0、空的list/dict)的话,就接续下去。因此结合先前反编译操作码的结果来看,会是这样的效果:

  • 如果__exit__返回真值,则走后面的POP一堆东西的逻辑(50)
    • 理论上,不会结束程序,打不打印异常看你在__exit__里有没有操作了
  • 如果__exit__返回非真值,就走下面的RERAISE指令(48),结束程序打印异常

首先来看RERAISE

case TARGET(RERAISE): {
    PyObject *exc = POP();
    PyObject *val = POP();
    PyObject *tb = POP();
    assert(PyExceptionClass_Check(exc));
    _PyErr_Restore(tstate, exc, val, tb);
    goto exception_unwind;
}

RERAISE实际把栈顶3个待raise的异常信息POP出来,并通过_PyErr_Restore重新设置当前线程出现的异常信息,然后又走到了exception_unwind。在exception_unwind的遍历代码块的while循环中,首先识别到先前BlockSetupEXCEPT_HANDLER代码段,调用UNWIND_EXCEPT_HANDLER把先前PUSH的异常栈顶的异常信息全给POP了,之后由于没有任何SETUP_FINALLY的标记,整个遍历代码块就结束了,最终就会把栈里剩下的值(__exit__)清掉,退出代码执行。 代码执行完毕之后,由于表示当前线程要抛出的异常的几个变量被_PyErr_Restore设置了,最终就会触发程序终止,并在stderr打印异常信息。

然后我们再看__exit__返回真值情况下那一堆POP操作,大概是这样:

  • 首先是3个POP_TOP,把待raise的异常信息POP
  • 然后是POP_EXCEPT,一方面会退出前面设置的EXCEPT_HANDLER代码段,另一方面会把先前PUSH进去的那个时刻的异常栈顶的信息给POP出来,并重新设置到异常栈顶上,保证异常信息恢复原样
  • 最后又来一个POP_TOP,就是把__exit__给POP掉

这样,整个with代码块的部分就执行完成了!

总结

with关键字分析了那么久,大家也能够看的明白,with本身其实相当于try/except/finally结构的变体。剖析with结构的同时,也不得不需要参考异常处理相关的代码逻辑。这篇文章与其说在讲with,不如说在讲一些异常处理相关的实现。 ​ 从上面的分析结果,我们就可以得出来: ​ 比如一个python代码段:

with a as b:
    xxx
    yyy
    zzz

就能够被简单地翻译为:

b = a.__enter__()
try:
    xxx
    yyy
    zzz
except exception_type, exception_value, exception_traceback:
    # handle exception
    ok = a.__exit__(exception_type, exception_value, exception_traceback)
    if not ok:
        # RERAISE
        raise (exception_type, exception_value, exception_traceback)
else:
    # normal ending
    a.__exit__(None, None, None)

翻译成这样,每一个学过一点python的同学都会很清楚地理解吧!

那么,如果我们要自己编写支持with语法的程序,可以参考下面的python代码:

import pprint


class WithTester(object):
    def __init__(self):
        self.__flag = 0

    def __enter__(self):
        self.__flag = 1
        print('[WithTester] triggered enter: %d' % self.__flag)
        return self.__flag + 99

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.__flag = 0
        print('[WithTester] triggered exit: %d\n%s' % (
            self.__flag,
            pprint.pformat({
                'exc_type': exc_type,
                'exc_val': exc_val,
                'exc_tb': exc_tb
            })
        ))
        return 'a true value'


def main():
    wt = WithTester()
    with wt as f:
        print(type(f))
        print(f)
        print('haha')
        raise KeyError('hehe')

支持with的实例,需要有只带self一个参数的__enter__函数,以及带self以及异常类型、异常值、异常traceback三个参数的__exit__函数。通过上面“代码翻译”的样式,不难看出,执行main函数会输出这样的结果,不带Exception报错:

[WithTester] triggered enter: 1
<class 'int'>
100
haha
[WithTester] triggered exit: 0
{'exc_tb': <traceback object at 0x000001D64D6F58C0>,
 'exc_type': <class 'KeyError'>,
 'exc_val': KeyError('hehe')}

看到了吧!with关键字的含义,就是这样简单。 ​

写在最后的话

相信通过Medium Python系列的讲解,大家应该会对python语言本身有了新的理解吧!在最后,笔者也推荐一本书:《Python源码剖析》,是一本老书,基于python2.5的,但是在python已经到3.10的今天,读起来仍然令人大开眼界。这个系列的许多分析,都参考了这本书的分析方法以及结论。

​ 知识是永远没有尽头的!做这个系列的过程中,笔者是一次又一次地在体验着这样的真理。今后的将来,希望大家一起勉励! ​