惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Forbes - Security
Forbes - Security
A
Arctic Wolf
M
MIT News - Artificial intelligence
T
Threat Research - Cisco Blogs
T
The Exploit Database - CXSecurity.com
C
CERT Recently Published Vulnerability Notes
NISL@THU
NISL@THU
L
Lohrmann on Cybersecurity
Martin Fowler
Martin Fowler
A
About on SuperTechFans
P
Palo Alto Networks Blog
Project Zero
Project Zero
The GitHub Blog
The GitHub Blog
WordPress大学
WordPress大学
Blog — PlanetScale
Blog — PlanetScale
博客园_首页
大猫的无限游戏
大猫的无限游戏
Cisco Talos Blog
Cisco Talos Blog
P
Proofpoint News Feed
D
DataBreaches.Net
Cyberwarzone
Cyberwarzone
T
Tor Project blog
IT之家
IT之家
P
Proofpoint News Feed
Help Net Security
Help Net Security
S
Securelist
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
C
CXSECURITY Database RSS Feed - CXSecurity.com
Microsoft Azure Blog
Microsoft Azure Blog
V2EX - 技术
V2EX - 技术
K
Kaspersky official blog
Hugging Face - Blog
Hugging Face - Blog
MongoDB | Blog
MongoDB | Blog
B
Blog
N
News and Events Feed by Topic
The Cloudflare Blog
S
Schneier on Security
P
Privacy & Cybersecurity Law Blog
T
The Blog of Author Tim Ferriss
Recorded Future
Recorded Future
Last Week in AI
Last Week in AI
The Last Watchdog
The Last Watchdog
Hacker News - Newest:
Hacker News - Newest: "LLM"
L
LangChain Blog
I
InfoQ
F
Full Disclosure
The Register - Security
The Register - Security
阮一峰的网络日志
阮一峰的网络日志
H
Hacker News: Front Page
V
V2EX

Posts on WKLKEN THINKING

apisix 中的 lrucache apisix 中的服务发现机制 apisix 中的负载均衡 apisix etcd机制 聊聊框架 关于 k8s 的 zero downtime deployment 一些建议 apisix 遇到的一些问题 关于在除夕前一天换了一个洗衣机的故事 Django DRF 性能优化 DRF 的一些实践 Part1: Serializer DRF继承关系图 Better Code: 关于接口的灵活性 新的仓库: wklken/naming 缓存使用的一些经验 Better Code: 抽象: 可扩展性与可维护性的抉择 Better Code: 异常时, 该提示用户哪些信息? Better Code: 更好的异常日志打印 Go: some libs Go: go-redis/cache升级的坑 Go: logrus性能提升 Go: gin validation 远程办公的一点总结 Go: 开发过程中的一些bug 项目管理实践: 风险驱动开发 Go: 一种error wrap调用链处理方式 漫谈技术选型 Go: 基于 apitest 做handler层单元测试 Go: go-sql-driver interpolateparams参数优化 [分享]深度工作 你需要更多的思考时间 Django项目重构小结 工作七年小结: 学习,生活及其他 [分享]bash日常: bash-utils 极客时间推广海报 2017总结: 予时光以意义 k8s APIServer源码: api注册详细细节 k8s APIServer源码: api注册主体流程 k8s APIServer源码: 服务启动 k8s APIServer源码: go-restful框架 重构 - 读书笔记(Python示例) 写给新人的沟通建议 vim 杂谈 - 关于快速编辑 vim 杂谈 - 关于移动 读书笔记-重构: 章11 处理概括关系 读书笔记-重构: 章10 简化函数调用 读书笔记-重构: 章9 简化表达式 读书笔记-重构: 章8 重新组织数据 读书笔记-重构: 章7 在对象之间搬移特性 读书笔记-重构: 章6 重新组织函数 Python 代码规范小结 [分享]关于vim ElasticSearch集群部署文档 Logstash+ElasticSearch处理mysql慢查询日志 [分享]关于代码调试DE那些事 Logstash+ElasticSearch+Kibana- 实现相对通用的数据收集分析 ELK维护的一些点(二) [分享]Python源码剖析-数据结构 一些Centos Python生产环境的部署命令 摘录<<6个月学会任何一种外语>> ELK 维护的一些点 也许是一个新的开始 一些vim的个性化配置 读书笔记-调试九法 这段时间的一些想法 Python 源码阅读 - 垃圾回收机制 我为什么要写博客 APUE笔记-第一章 UNIX基础知识 Python源码阅读-内存管理机制(二) Python源码阅读-内存管理机制(一) Python-基础-数据结构小结 '活动'设计的一些trick 一些简单的Python测试题 我的tmux配置及说明【k-tmux】 Review and Restart 工作四周年小结 vim插件: surround & repeat[成对符号编辑] vim插件: gundo[时光机] vim插件: expand-region[区域选中] vim插件: quickrun[快速执行] vim插件: trailing-whitespace[行尾空格处理] vim插件: closetag[成对标签补全] vim插件: ctrlp[文件搜索] vim插件: airline[状态栏增强] vim插件: theme[主题] vim插件: tagbar[大纲式导航] vim插件: nerdcommenter[快速注释] vim插件: rainbow_parentheses[括号高亮] vim插件: syntastic[语法检查] vim插件: delimitmate[符号自动补全] vim插件: matchit[成对标签跳转] vim插件: easy-align[快速对齐] vim插件: multiple-cursors[多光标操作] vim插件: vim-signature[快速标记跳转] vim插件: easymotion[快速跳转] vim插件: vundle[管理插件] Elasticsearch几个问题的解决 分享一份 Vim 简介PPT k-vim 更新9.0版本 关于知识管理工具的思考 Logstash+ElasticSearch+Kibana处理nginx访问日志
Python源码阅读-闭包的实现
2015-09-04 · via Posts on WKLKEN THINKING

闭包

e.g.

def add(x):
    def do_add(value):
        return x + value
    return do_add

add_5 = add(5)
print add_5(1)  # 6
print add_5(2)  # 7

需要回答, 什么是闭包, CPython底层是如何实现的?

PyCodeObject

typedef struct {
    PyObject_HEAD
    int co_argcount;		/* #arguments, except *args */
    int co_nlocals;		    /* #local variables */
    int co_stacksize;		/* #entries needed for evaluation stack */
    int co_flags;		    /* CO_..., see below */
    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 doesn't count for hash/cmp */
    PyObject *co_filename;	/* string (where it was loaded from) */
    PyObject *co_name;		/* string (name, for reference) */
    int co_firstlineno;		/* first source line number */
    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 */
} PyCodeObject;

我们关注两个, co_freevarsco_cellvars

co_freevars, 保存使用了的外层作用域中的变量名集合 (编译时就知道的! 被嵌套的时候有用)

co_cellvars, 保存嵌套作用域中使用的变量名集合, (编译时就知道的! 包含嵌套函数时有用)

对于我们上面的那个示例, add是外层函数, do_add是嵌套函数, 我们可以通过func_code打印看看

def add(x):    # 外层函数
    # 外层函数, 没有使用了外层作用域变量, 被嵌套函数使用了'x'
    print add.func_code.co_freevars    # ()
    print add.func_code.co_cellvars    # ('x',)

    def do_add(value):  # 嵌套函数
        return x + value

    # 内层函数, 使用了外层作用域便令'x', 没有嵌套函数故嵌套作用域变量名集合空
    print do_add.func_code.co_freevars # ('x',)
    print do_add.func_code.co_cellvars # ()
    return do_add

此时图示

closure

这时候, 只是记录了使用到的变量名, 标记下是否使用了外层的/被内层使用的变量

具体的值是在运行时确定的, 例如

此时x=5, 这个是在add的名字空间里面的, 那么, x=5是怎么传递到嵌套函数内? 嵌套函数又是如何知晓x的值?

记住这两个问题, 然后我们首先来看一个新的数据结构

PyCellObject

  typedef struct {
    PyObject_HEAD
    PyObject *ob_ref;   /* Content of the cell or NULL when empty */ => 指向一个PyObject
  } PyCellObject;


  PyObject *
  PyCell_New(PyObject *obj)
  {
      PyCellObject *op;

      op = (PyCellObject *)PyObject_GC_New(PyCellObject, &PyCell_Type);
      if (op == NULL)
          return NULL;
      op->ob_ref = obj;  //建立关系
      Py_XINCREF(obj);

      _PyObject_GC_TRACK(op);
      return (PyObject *)op;
  }

这是个很简单的基本对象, 有一个ob_ref指向另一个PyObject, 仅此而已

图示

closure

作用呢?

值的确认与传递过程

调用

此时, 开始调用函数

CALL_FUNCTION

=> x = call_function(&sp, oparg);

=> x = fast_function(func, pp_stack, n, na, nk);

      return PyEval_EvalCodeEx(co, globals,
                               (PyObject *)NULL, (*pp_stack)-n, na,
                               (*pp_stack)-2*nk, nk, d, nd,
                               PyFunction_GET_CLOSURE(func));

=>

PyEval_EvalCodeEx

add(5), 此时其co_cellvars = ('x',) 非空, 将会执行的逻辑代码


      /* Allocate and initialize storage for cell vars, and copy free
         vars into frame.  This isn't too efficient right now. */
      if (PyTuple_GET_SIZE(co->co_cellvars)) {
          int i, j, nargs, found;
          char *cellname, *argname;
          PyObject *c;

          nargs = co->co_argcount;
          if (co->co_flags & CO_VARARGS)
              nargs++;
          if (co->co_flags & CO_VARKEYWORDS)
              nargs++;

          /* Initialize each cell var, taking into account
             cell vars that are initialized from arguments.

             Should arrange for the compiler to put cellvars
             that are arguments at the beginning of the cellvars
             list so that we can march over it more efficiently?
          */

          // for 循环遍历 co_cellvars = ('x', ), i = 0
          for (i = 0; i < PyTuple_GET_SIZE(co->co_cellvars); ++i) {

              // cellname = 'x'
              cellname = PyString_AS_STRING(
                  PyTuple_GET_ITEM(co->co_cellvars, i));
              found = 0;

              // 遍历函数的参数变量, narg=1, j=0
              for (j = 0; j < nargs; j++) {

                  // 访问当前名字空间
                  argname = PyString_AS_STRING(
                      PyTuple_GET_ITEM(co->co_varnames, j));

                  // 匹配上了
                  if (strcmp(cellname, argname) == 0) {

                      // new 一个 PyCellObject, ob_ref指向变量的PyObject
                      c = PyCell_New(GETLOCAL(j));
                      if (c == NULL)
                          goto fail;

                      // #define GETLOCAL(i)     (fastlocals[i])
                      // fastlocals = f->f_localsplus;
                      // 即 f->f_localsplus[co->co_nlocals + i] = c, 相当于放到下一层freevars变量
                      GETLOCAL(co->co_nlocals + i) = c;
                      found = 1;
                      break;
                  }
              }

              // 没有匹配, 给个指向NULL的PyCellObject, 先New一个对象占位
              if (found == 0) {
                  c = PyCell_New(NULL);
                  if (c == NULL)
                      goto fail;
                  SETLOCAL(co->co_nlocals + i, c); //注意内存地址
              }
          }
      }

逻辑即, 如果发现当前函数co_cellvars非空, 即表示存在被内层函数调用的变量, 那么遍历这个co_cellvars集合, 拿到集合中每个变量名在当前名字空间中的值, 然后放到当前函数的f->f_localsplus中.

这里, 我们可以知道x=5被放进去了

为什么放到f->f_localsplus中呢?

看看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 */

    /* If an exception is raised in this frame, the next three are used to
     * record the exception info (if any) originally in the thread state.  See
     * comments before set_exc_info() -- it's not obvious.
     * Invariant:  if _type is NULL, then so are _value and _traceback.
     * Desired invariant:  all three are NULL, or all three are non-NULL.  That
     * one isn't currently true, but "should be".
     */
    PyObject *f_exc_type, *f_exc_value, *f_exc_traceback;

    PyThreadState *f_tstate;
    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 */
    PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */
    PyObject *f_localsplus[1];	/* locals+stack, dynamically sized */
} PyFrameObject;

注意f_localsplus

f_localsplus为一个PyObject的指针数组,大小为1。

c语言中, 当申请一个大小超过sizeof(PyFrameObject)的结构体对象时,超过的部分就自动分配给f_localsplus

创建过程

call_function的时候, new了一个PyFrameObject

f = PyFrame_New(tstate, co, globals, locals);

=>

PyFrameObject *
PyFrame_New(PyThreadState *tstate, PyCodeObject *code, PyObject *globals,
            PyObject *locals)
{
        Py_ssize_t extras, ncells, nfrees;
        ncells = PyTuple_GET_SIZE(code->co_cellvars);
        nfrees = PyTuple_GET_SIZE(code->co_freevars);
        extras = code->co_stacksize + code->co_nlocals + ncells +
            nfrees;

=>

f = PyObject_GC_NewVar(PyFrameObject, &PyFrame_Type, extras);

f_localsplus => 局部变量 + cell对象 + free对象 + 运行时栈

原因: 因为函数中的局部变量总是固定不变的, 在编译时就能确定局部变量使用的内存空间的位置, 也能确定访问局部变量的字节码应该如何访问内存, 有了这些信息, Python就能借助静态的方法实现局部变量, 而不是动态查找PyDictObject, 提高执行效率

closure

示例函数的f_localsplus

看一下上面赋值用的宏定义

  fastlocals = f->f_localsplus;
  #define GETLOCAL(i)     (fastlocals[i])
  #define SETLOCAL(i, value)      do { PyObject *tmp = GETLOCAL(i); \
                                       GETLOCAL(i) = value; \
                                       Py_XDECREF(tmp); } while (0)

最终得到

closure

接下去呢? CALL_FUNCTION最后怎么处理将cell传入嵌套函数?

传递

CALL_FUNCTION 完成new一个PyFrameObject之后,

最终执行这个frame

retval = PyEval_EvalFrameEx(f,0);

PyEval_EvalFrameEx

  PyObject *
  PyEval_EvalFrameEx(PyFrameObject *f, int throwflag)
  {

    fastlocals = f->f_localsplus;
    freevars = f->f_localsplus + co->co_nlocals;


=> 此时涉及op_code的执行了

查看一下dis的结果

def add(x):
    def do_add(value):
        return x + value
    return do_add

  5           0 LOAD_CLOSURE             0 (x)
              3 BUILD_TUPLE              1
              6 LOAD_CONST               1 (<code object do_add at 0x10c9cec30, file "a.py", line 5>)
              9 MAKE_CLOSURE             0
             12 STORE_FAST               1 (do_add)

  7          15 LOAD_FAST                1 (do_add)
             18 RETURN_VALUE

首先LOAD_CLOSURE 0

          case LOAD_CLOSURE:
              x = freevars[oparg];
              Py_INCREF(x);
              PUSH(x);
              if (x != NULL) continue;
              break;

入栈, 此时得到一个PyCellObject, 指向2, name='x'

LOAD_CLOSURE 在编译时会根据嵌套函数中 co_freevars, 决定了取得参数位置和个数

然后, BUILD_TUPLE, 将cell对象打包成tuple, 得到('x', )

然后, 开始, 载入嵌套函数do_add, 入栈

调用MAKE_CLOSURE

          case MAKE_CLOSURE:
          {
              v = POP(); /* code object */  // do_add函数
              x = PyFunction_New(v, f->f_globals); //绑定global名字空间
              // 到这里, 得到一个PyFunctionObject

              Py_DECREF(v);
              if (x != NULL) {
                  v = POP();   // 得到tuple, ('x', )

                  // 注意这里
                  if (PyFunction_SetClosure(x, v) != 0) {
                      /* Can't happen unless bytecode is corrupt. */
                      why = WHY_EXCEPTION;
                  }
                  Py_DECREF(v);
              }
              ......
          }

来关注一下 PyFunction_SetClosure

int
PyFunction_SetClosure(PyObject *op, PyObject *closure)
{
    ...
    Py_XDECREF(((PyFunctionObject *) op) -> func_closure);
    ((PyFunctionObject *) op) -> func_closure = closure;  // 注意这里
    return 0;
}

do_addPyFunctionObjectfunc_closure指向一个tuple

注意: 这时候, 外层变量已经固定下来了!!!!!!

然后, 在嵌套函数被调用的时候

CALL_FUNCTION

=>

x = call_function(&sp, oparg);

=>

x = fast_function(func, pp_stack, n, na, nk);

=>

      return PyEval_EvalCodeEx(co, globals,
                               (PyObject *)NULL, (*pp_stack)-n, na,
                               (*pp_stack)-2*nk, nk, d, nd,
                               PyFunction_GET_CLOSURE(func));

看下PyFunction_GET_CLOSURE

  #define PyFunction_GET_CLOSURE(func) \
      (((PyFunctionObject *)func) -> func_closure)

然后, 进入 PyEval_EvalCodeEx, 注意这里的closure参数即上一步取出来的func_closure, 即外层函数传进来的tuple

  PyObject *
  PyEval_EvalCodeEx(PyCodeObject *co, PyObject *globals, PyObject *locals,
             PyObject **args, int argcount, PyObject **kws, int kwcount,
             PyObject **defs, int defcount, PyObject *closure)
{
      ......
      //  嵌套函数do_add, 使用到了外层函数的变量, 所以co->co_freevars非空, 这里得到 ('x', )
      if (PyTuple_GET_SIZE(co->co_freevars)) {
          int i;
          for (i = 0; i < PyTuple_GET_SIZE(co->co_freevars); ++i) {
              // 顺序是一致的
              PyObject *o = PyTuple_GET_ITEM(closure, i);
              Py_INCREF(o);
              // 放到freevars里面, 编译时已经确定了顺序
              // 在上一步多LOAD_CLOSURE => tuple 已经保证了顺序
              freevars[PyTuple_GET_SIZE(co->co_cellvars) + i] = o;
          }
      }
      ......

最后, 再来看一个闭包的dis

注意BUILD_TUPLE

def add(x, y):

    def do_add(value):
        return x + value

    def do_add2(value):
        return y + value

    def do_add3(value):
        return x + y + value

    return do_add

dis结果

 18           0 LOAD_CLOSURE             0 (x)
              3 BUILD_TUPLE              1
              6 LOAD_CONST               1 (<code object do_add at 0x10560dc30, file "a.py", line 18>)
              9 MAKE_CLOSURE             0
             12 STORE_FAST               2 (do_add)

 21          15 LOAD_CLOSURE             1 (y)
             18 BUILD_TUPLE              1
             21 LOAD_CONST               2 (<code object do_add2 at 0x10560d8b0, file "a.py", line 21>)
             24 MAKE_CLOSURE             0
             27 STORE_FAST               3 (do_add2)

 24          30 LOAD_CLOSURE             0 (x)
             33 LOAD_CLOSURE             1 (y)
             36 BUILD_TUPLE              2
             39 LOAD_CONST               3 (<code object do_add3 at 0x10560e3b0, file "a.py", line 24>)
             42 MAKE_CLOSURE             0
             45 STORE_FAST               4 (do_add3)

 32          48 LOAD_FAST                2 (do_add)
             51 RETURN_VALUE