# 关键源码

# PyCodeObject

/* Bytecode object */
typedef struct {
    PyObject_HEAD
    int co_argcount;            /* #arguments, except *args */
    int co_kwonlyargcount;      /* #keyword only arguments */
    int co_nlocals;             /* #local variables */
    int co_stacksize;           /* #entries needed for evaluation stack */
    int co_flags;               /* CO_..., see below */
    int co_firstlineno;         /* first source line number */
    PyObject *co_code;          /* instruction opcodes */
    PyObject *co_consts;        /* list (constants used) */
    PyObject *co_names;         /* list of strings (names used) */
    PyObject *co_varnames;      /* tuple of strings (local variable names) */
    PyObject *co_freevars;      /* tuple of strings (free variable names) */
    PyObject *co_cellvars;      /* tuple of strings (cell variable names) */
    /* The rest aren't used in either hash or comparisons, except for co_name,
       used in both. This is done to preserve the name and line number
       for tracebacks and debuggers; otherwise, constant de-duplication
       would collapse identical functions/lambdas defined on different lines.
    */
    Py_ssize_t *co_cell2arg;    /* Maps cell vars which are arguments. */
    PyObject *co_filename;      /* unicode (where it was loaded from) */
    PyObject *co_name;          /* unicode (name, for reference) */
    PyObject *co_lnotab;        /* string (encoding addr<->lineno mapping) See
                                   Objects/lnotab_notes.txt for details. */
    void *co_zombieframe;       /* for optimization only (see frameobject.c) */
    PyObject *co_weakreflist;   /* to support weakrefs to code objects */
    /* Scratch space for extra data relating to the code object.
       Type is a void* to keep the format private in codeobject.c to force
       people to go through the proper APIs. */
    void *co_extra;
} PyCodeObject;

# PyFrameObject


typedef 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 */
} PyFrameObject;

# PyFunctionObject

typedef struct {
    PyObject_HEAD
    PyObject *func_code;        /* A code object, the __code__ attribute */
    PyObject *func_globals;     /* 全局名字空间:A dictionary (other mappings won't do) */
    PyObject *func_defaults;    /* NULL or a tuple */
    PyObject *func_kwdefaults;  /* NULL or a dict */
    PyObject *func_closure;     /* NULL or a tuple of cell objects */
    PyObject *func_doc;         /* The __doc__ attribute, can be anything */
    PyObject *func_name;        /* The __name__ attribute, a string object */
    PyObject *func_dict;        /* 局部名字空间:The __dict__ attribute, a dict or NULL */
    PyObject *func_weakreflist; /* List of weak references */
    PyObject *func_module;      /* The __module__ attribute, can be anything */
    PyObject *func_annotations; /* Annotations, a dict or NULL */
    PyObject *func_qualname;    /* The qualified name */

    /* Invariant:
     *     func_closure contains the bindings for func_code->co_freevars, so
     *     PyTuple_Size(func_closure) == PyCode_GetNumFree(func_code)
     *     (func_closure may be NULL if PyCode_GetNumFree(func_code) == 0).
     */
} PyFunctionObject;

# ByteCode

# MAKE_FUNCTION

	# Python/ceval.c
        TARGET(MAKE_FUNCTION) {
            PyObject *qualname = POP();		// 从栈顶弹出关键参数-函数名、代码对象
            PyObject *codeobj = POP();
            PyFunctionObject *func = (PyFunctionObject *)	// 创建函数对象并继承帧对象的全局名字空间
                PyFunction_NewWithQualName(codeobj, f->f_globals, qualname);

            Py_DECREF(codeobj);
            Py_DECREF(qualname);
            if (func == NULL) {
                goto error;
            }

            if (oparg & 0x08) {
                assert(PyTuple_CheckExact(TOP()));
                func ->func_closure = POP();	// 如果是闭包函数,从栈顶取闭包变量
            }
            if (oparg & 0x04) {
                assert(PyDict_CheckExact(TOP()));
                func->func_annotations = POP();	// 如果函数包含注释,从栈顶取注释
            }
            if (oparg & 0x02) {
                assert(PyDict_CheckExact(TOP()));
                func->func_kwdefaults = POP();	// 如果函数参数有默认值,从栈顶取默认值
            }
            if (oparg & 0x01) {
                assert(PyTuple_CheckExact(TOP()));
                func->func_defaults = POP();	// 如果函数参数有默认值,从栈顶取默认值
            }

            PUSH((PyObject *)func);
            DISPATCH();
        }

# CALL_FUNCTION

负责函数调用。

CALL_FUNCTION​​ 是 Python 字节码指令之一,它的作用是调用函数。具体来说,CALL_FUNCTION​​ 从栈顶依次弹出 n​​ 个参数,然后从栈顶弹出一个函数对象,将这 n​​ 个参数传递给该函数并调用它。调用完成后,将函数返回值压入栈顶。

CALL_FUNCTION​在Python/ceval.c​ 中处理,它主要是调用 call_function​函数完成工作。call_function​函数根据被调用对象类型区别处理,可分为 类方法函数对象普通可调用对象 等等。

如果被调用对象是函数对象,则调用位于 Objects/call.c​ 中的 _PyFunction_FastCallKeywords​ 函数,而它则进一步调用位于 Python/ceval.c​ 的 _PyEval_EvalCodeWithName​函数。

_PyEval_EvalCodeWithName​函数先为目标函数 co_process​创建 栈帧 对象 f,然后检查代码对象标识。若代码对象带有 CO_GENERATOR​、CO_COROUTINE​或 CO_ASYNC_GENERATOR​标识,便创建生成器并返回。

以下是一些 CALL_FUNCTION​ 指令的使用示例:

  1. 调用无参数函数:

    在这个示例中,CALL_FUNCTION 0​ 指令将调用 my_func​ 函数,并且不传递任何参数。

    def my_func():
        print("Hello, world!")
    
    CALL_FUNCTION 0
    
  2. 调用有参数函数:

    在这个示例中,CALL_FUNCTION 2​ 指令将调用 add​ 函数,并传递两个参数。先通过 LOAD_CONST​ 指令将常量 1​ 和 2​ 压入栈顶,然后执行 CALL_FUNCTION 2​ 指令调用 add​ 函数,并将栈顶的两个元素作为参数传递给该函数。

    def add(a, b):
        return a + b
    
    LOAD_CONST 1  # 压入常量 1
    LOAD_CONST 2  # 压入常量 2
    CALL_FUNCTION 2
    
  3. 调用方法:

    在这个示例中,CALL_FUNCTION 1​ 指令将调用 append​ 方法,并传递一个参数。首先通过 LOAD_NAME​ 指令将变量 my_list​ 压入栈顶,然后通过 LOAD_ATTR​ 指令获取其 append​ 属性,并将该方法对象压入栈顶。接着通过 LOAD_CONST​ 指令将常量 4​ 压入栈顶,最后执行 CALL_FUNCTION 1​ 指令调用 append​ 方法,并将栈顶的一个元素作为参数传递给该方法。

    my_list = [1, 2, 3]
    my_list.append(4)
    
    LOAD_NAME 0  # 压入名字 "my_list"
    LOAD_ATTR 1  # 获取 my_list 的 "append" 属性
    LOAD_CONST 2  # 压入常量 4
    CALL_FUNCTION 1
    
    

# LOAD_CONST

在Python字节码中,LOAD_CONST​ 操作码的作用是将一个常量压入操作数栈中,常量的值存在常量表中,常量可以是数字、字符串、元组等不可变的对象。

LOAD_CONST​ 操作码的入参是常量在常量表中的索引。当Python解释器执行到 LOAD_CONST​ 时,会根据给定的索引获取常量值,并将该值推入操作数栈中。

例如,假设有以下代码:

def example():
    return 42

该代码的字节码可以通过 dis​ 模块来查看:

import dis

def example():
    return 42

dis.dis(example)

输出结果为:

  3           0 LOAD_CONST               1 (42)
              2 RETURN_VALUE

可以看到,在字节码的第一行(索引为0)中,LOAD_CONST​ 操作码将常量表中索引为1的常量值 42​ 推入操作数栈中。

因此,当函数 example​ 被调用时,解释器会执行字节码中的 LOAD_CONST​ 操作码,将常量值 42​ 压入操作数栈中,然后执行 RETURN_VALUE​ 操作码将结果返回。

# LOAD_FAST

由于函数有多少局部变量是固定的,代码编译时就能确定。因此,没有必要用字典来实现局部名字空间,只需把局部变量依次编号,保存在栈底即可。这样一来,通过编号即可快速存取局部变量,效率比字典更高。于此对应,有一个特殊的字节码 LOAD_FAST 用于加载局部变量,以操作数的编号为操作数。

图片描述

LOAD_FAST 是 Python 字节码指令之一,用于从当前函数的本地命名空间中加载变量的值。具体而言:

  1. LOAD_FAST 的操作数是一个变量名,在字节码指令中作为索引来查找变量名在本地命名空间中对应的值。
  2. 如果变量名不存在,则会引发 NameError 异常。
  3. LOAD_FAST 操作指令将值加载到运行时数据栈的栈顶。
  4. 在函数的本地命名空间中查找变量名的过程是基于 LIFO 的栈帧结构实现的,即最后定义的变量名在栈顶。
  5. LOAD_FAST 指令的效率较高,因为它可以通过直接从栈帧对象的局部变量表中加载变量值而无需进行额外的查找或解析操作。

# STORE_FAST

STORE_FAST​ 是Python虚拟机的一条字节码指令,用于将数据存储到局部变量中。

STORE_FAST​ 的入参是一个整数,代表局部变量在当前函数的局部命名空间中的位置。这个整数由编译器在编译时确定,一般是根据该变量在源代码中出现的位置以及该函数中的其他变量数量来计算得出。它指示了该变量在栈帧对象的 f_locals​ 字典中的位置。

STORE_FAST​ 的行为是将栈顶的数值弹出,并存储到局部变量中。因此,对于 STORE_FAST​ 指令来说,栈顶的值就是要存储到局部变量中的值。它使用被弹出的值来更新 f_locals​ 字典中对应的局部变量。

以下是一个使用 STORE_FAST​ 指令的示例代码:

def example_function(x):
    y = 2
    z = x + y
    return z

在上面的示例代码中,变量 x​ 在栈的位置是 0,y​ 在位置 1,z​ 在位置 2。因此,当虚拟机执行到 y = 2​ 这行代码时,它会将常量 2 压入栈中,然后执行 STORE_FAST 1​ 操作,将栈顶的值 2 存储到位置 1 对应的局部变量中。

类似地,当执行到 z = x + y​ 这行代码时,虚拟机会将变量 x​ 和 y​ 分别从位置 0 和位置 1 取出,并执行相应的计算,将结果压入栈顶。然后执行 STORE_FAST 2​ 操作,将栈顶的值存储到位置 2 对应的局部变量中。

最后,当虚拟机执行到 return z​ 这行代码时,它会将变量 z​ 从位置 2 取出,并将其作为函数的返回值。

# LOAD_DEREF

PyFrameObject 结构体最后部分是不固定的,依次存放着静态局部名字空间、闭包名字空间以及临时栈。

由于函数局部变量、闭包变量个数在编译阶段就能确定,运行时并不会增减,因此无须用 dict 对象来保存。相反,将这些变量依次排列保存在数组中,然后通过数组下标来访问即可。这就是所谓的静态名字空间。

对于局部变量 n ,数组对应的槽位保存着整数对象 1 的地址,表示 n 与 1 绑定。而闭包变量 x 则略有差别,槽位不直接保存整数对象 10 ,而是通过一个 PyCellObject 间接与整数对象 10 绑定。

这么做是为了保证nonloacl的修改能生效而不是出去就没了。

函数对象 PyFunctionObject 中有一个字段 func_closure ,保存着函数所有闭包变量。我们可以通过名字 closure 可以访问到这个底层结构体字段:

>>> add10.__closure__
(<cell at 0x10dc09e28: int object at 0x10da161a0>,)

这是一个由 PyCellObject 组成的元组,PyCellObject 则保存着闭包变量的值。当函数调用发生时,Python 虚拟机创建 PyFrameObject 对象,并从函数对象取出该元组,依次填充相关静态槽位。

图片描述

通过 cell_contents 属性可以访问闭包变量值:

>>> add10.__closure__[0]
<cell at 0x10dc09e28: int object at 0x10da161a0>
>>> add10.__closure__[0].cell_contents
10