详解Python locals()的陷阱


Posted in Python onMarch 26, 2019

在工作中, 有时候会遇到一种情况: 动态地进行变量赋值, 不管是局部变量还是全局变量, 在我们绞尽脑汁的时候, Python已经为我们解决了这个问题.

Python的命名空间通过一种字典的形式来体现, 而具体到函数也就是locals() 和 globals(), 分别对应着局部命名空间和全局命名空间. 于是, 我们也就能通过这些方法去实现我们"动态赋值"的需求.

例如:

def test():
  globals()['a2'] = 4
test()
print a2  # 输出 4

很自然, 既然 globals能改变全局命名空间, 那理所当然locals应该也能修改局部命名空间.修改函数内的局部变量.

但事实真是如此吗? 不是!

def aaaa():
  print locals()
  for i in ['a', 'b', 'c']:
    locals()[i] = 1
  print locals()
  print a
aaaa()

输出:

{}
{'i': 'c', 'a': 1, 'c': 1, 'b': 1}
Traceback (most recent call last):
  File "5.py", line 17, in <module>
    aaaa()
  File "5.py", line 16, in aaaa
    print a
NameError: global name 'a' is not defined

程序运行报错了!

但是在第二次print locals()很清楚能够看到, 局部空间是已经有那些变量了, 其中也有变量a并且值也为1, 但是为什么到了print a却报出NameError异常?

再看一个例子:

def aaaa():
  print locals()
  s = 'test'          # 加入显示赋值 s    
  for i in ['a', 'b', 'c']:
    locals()[i] = 1
  print locals()
  print s            # 打印局部变量 s 
  print a
aaaa()

输出:

{}
{'i': 'c', 'a': 1, 's': 'test', 'b': 1, 'c': 1}
test
Traceback (most recent call last):
  File "5.py", line 19, in <module>
    aaaa()
  File "5.py", line 18, in aaaa
    print a
NameError: global name 'a' is not defined

上下两段代码, 区别就是, 下面的有显示赋值的代码, 虽然也是同样触发了NameError异常, 但是局部变量s的值被打印了出来.

这就让我们觉得很纳闷, 难道通过locals()改变局部变量, 和直接赋值有不同? 想解决这个问题, 只能去看程序运行的真相了, 又得上大杀器dis~

根源探讨

直接对第二段代码解析:

13      0 LOAD_GLOBAL       0 (locals)
       3 CALL_FUNCTION      0
       6 PRINT_ITEM
       7 PRINT_NEWLINE

 14      8 LOAD_CONST        1 ('test')
       11 STORE_FAST        0 (s)

 15     14 SETUP_LOOP       36 (to 53)
       17 LOAD_CONST        2 ('a')
       20 LOAD_CONST        3 ('b')
       23 LOAD_CONST        4 ('c')
       26 BUILD_LIST        3
       29 GET_ITER
    >>  30 FOR_ITER        19 (to 52)
       33 STORE_FAST        1 (i)

 16     36 LOAD_CONST        5 (1)
       39 LOAD_GLOBAL       0 (locals)
       42 CALL_FUNCTION      0
       45 LOAD_FAST        1 (i)
       48 STORE_SUBSCR
       49 JUMP_ABSOLUTE      30
    >>  52 POP_BLOCK

 17   >>  53 LOAD_GLOBAL       0 (locals)
       56 CALL_FUNCTION      0
       59 PRINT_ITEM
       60 PRINT_NEWLINE

 18     61 LOAD_FAST        0 (s)
       64 PRINT_ITEM
       65 PRINT_NEWLINE

 19     66 LOAD_GLOBAL       1 (a)
       69 PRINT_ITEM
       70 PRINT_NEWLINE
       71 LOAD_CONST        0 (None)
       74 RETURN_VALUE
None

在上面的字节码可以看到:

  1. locals() 对应的字节码是: LOAD_GLOBAL
  2. s='test' 对应的字节码是: LOAD_CONST 和 STORE_FAST
  3. print s 对应的字节码是: LOAD_FAST
  4. print a 对应的字节码是: LOAD_GLOBAL

从上面罗列出来的几个关键语句的字节码可以看出, 直接赋值/读取 和 通过locals()赋值/读取 本质是很大不同的. 那么触发NameError异常, 是否证明通过 locals()[i] = 1 存储的值, 和真正的局部命名空间 是不同的两个位置?

想要回答这个问题, 我们得先确定一个东西, 就是真正的局部命名空间如何获取? 其实这个问题, 在上面的字节码上, 已经给出了标准答案了!

真正的局部命名空间, 其实是存在 STORE_FAST 这个对应的数据结构里面. 这个是什么鬼, 这个需要源码来解答:

// ceval.c 从上往下, 依次是相应函数或者变量的定义
// 指令源码
TARGET(STORE_FAST)
{
  v = POP();
  SETLOCAL(oparg, v);
  FAST_DISPATCH();
}
--------------------
// SETLOCAL 宏定义   
#define SETLOCAL(i, value)   do { PyObject *tmp = GETLOCAL(i); \
                   GETLOCAL(i) = value; \
                   Py_XDECREF(tmp); } while (0)
-------------------- 
// GETLOCAL 宏定义                  
#define GETLOCAL(i)   (fastlocals[i])   

-------------------- 
// fastlocals 真面目
PyObject * PyEval_EvalFrameEx(PyFrameObject *f, int throwflag){
  // 省略其他无关代码
  fastlocals = f->f_localsplus;
....
}

看到这里, 应该就能明确了, 函数内部的局部命名空间, 实际是就是帧对象的f的成员f_localsplus, 这是一个数组, 了解函数创建的童鞋可能会比较清楚, 在CALL_FUNCTION时, 会对这个数组进行初始化, 将形参赋值什么都会按序塞进去, 在字节码 18 61 LOAD_FAST 0 (s)中, 第四列的0, 就是将f_localsplus第 0 个成员取出来, 也就是值 "s".

所以STORE_FAST才是真正的将变量存入局部命名空间, 那locals()又是什么鬼? 为什么看起来就跟真的一样?

这个就需要分析locals, 对于这个, 字节码可能起不了作用, 直接去看内置函数如何定义的吧:

// bltinmodule.c
static PyMethodDef builtin_methods[] = {
  ...
  // 找到 locals 函数对应的内置函数是 builtin_locals 
  {"locals",     (PyCFunction)builtin_locals,   METH_NOARGS, locals_doc},
  ...
}

-----------------------------

// builtin_locals 的定义
static PyObject *
builtin_locals(PyObject *self)
{
  PyObject *d;

  d = PyEval_GetLocals();
  Py_XINCREF(d);
  return d;
}
-----------------------------

PyObject *
PyEval_GetLocals(void)
{
  PyFrameObject *current_frame = PyEval_GetFrame(); // 获取当前堆栈对象
  if (current_frame == NULL)
    return NULL;
  PyFrame_FastToLocals(current_frame); // 初始化和填充 f_locals
  return current_frame->f_locals;
}
-----------------------------

// 初始化和填充 f_locals 的具体实现
void
PyFrame_FastToLocals(PyFrameObject *f)
{
  /* Merge fast locals into f->f_locals */
  PyObject *locals, *map;
  PyObject **fast;
  PyObject *error_type, *error_value, *error_traceback;
  PyCodeObject *co;
  Py_ssize_t j;
  int ncells, nfreevars;
  if (f == NULL)
    return;
  locals = f->f_locals;
  
  // 如果locals为空, 就新建一个字典对象
  if (locals == NULL) {
    locals = f->f_locals = PyDict_New(); 
    if (locals == NULL) {
      PyErr_Clear(); /* Can't report it :-( */
      return;
    }
  }
  
  co = f->f_code;
  map = co->co_varnames;
  if (!PyTuple_Check(map))
    return;
  PyErr_Fetch(&error_type, &error_value, &error_traceback);
  fast = f->f_localsplus;
  j = PyTuple_GET_SIZE(map);
  if (j > co->co_nlocals)
    j = co->co_nlocals;
    
  // 将 f_localsplus 写入 locals
  if (co->co_nlocals)
    map_to_dict(map, j, locals, fast, 0);
  ncells = PyTuple_GET_SIZE(co->co_cellvars);
  nfreevars = PyTuple_GET_SIZE(co->co_freevars);
  if (ncells || nfreevars) {
    // 将 co_cellvars 写入 locals
    map_to_dict(co->co_cellvars, ncells,
          locals, fast + co->co_nlocals, 1);
          
    if (co->co_flags & CO_OPTIMIZED) {
      // 将 co_freevars 写入 locals
      map_to_dict(co->co_freevars, nfreevars,
            locals, fast + co->co_nlocals + ncells, 1);
    }
  }
  PyErr_Restore(error_type, error_value, error_traceback);
}

从上面PyFrame_FastToLocals已经看出来, locals() 实际上做了下面几件事:

  1. 判断帧对象 的 f_f->f_locals 是否为空, 若是, 则新建一个字典对象.
  2. 分别将 localsplus, co_cellvars 和 co_freevars 写入 f_f->f_locals.

在这简单介绍下上面几个分别是什么鬼:

  1. localsplus: 函数参数(位置参数+关键字参数), 显示赋值的变量.
  2. co_cellvars 和 co_freevars: 闭包函数会用到的局部变量.

结论

通过上面的源码, 我们已经很明确知道locals() 看到的, 的确是函数的局部命名空间的内容, 但是它本身不能代表局部命名空间, 这就好像一个代理, 它收集了A, B, C的东西, 展示给我看, 但是我却不能简单的通过改变这个代理, 来改变A, B, C真正拥有的东西!

这也就是为什么, 当我们通过locals()[i] = 1的方式去动态赋值时, print a却触发了NameError异常, 而相反的, globals()确实真正的全局命名空间, 所以一般会说

locals() 只读, globals() 可读可写

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Python 相关文章推荐
python使用scrapy解析js示例
Jan 23 Python
python简单分割文件的方法
Jul 30 Python
python 中的list和array的不同之处及转换问题
Mar 13 Python
Python的多维空数组赋值方法
Apr 13 Python
pytorch构建网络模型的4种方法
Apr 13 Python
Python实现网站表单提交和模板
Jan 15 Python
Python自动抢红包教程详解
Jun 11 Python
Iconfont(矢量图标)+iconmoon(图标svg互转)配合javascript实现社交分享系统
Apr 21 Python
python如何输出反斜杠
Jun 18 Python
使用Keras构造简单的CNN网络实例
Jun 29 Python
浅谈keras中的keras.utils.to_categorical用法
Jul 02 Python
用Python实现职工信息管理系统
Dec 30 Python
python 为什么说eval要慎用
Mar 26 #Python
Python eval的常见错误封装及利用原理详解
Mar 26 #Python
Python骚操作之动态定义函数
Mar 26 #Python
python 将有序数组转换为二叉树的方法
Mar 26 #Python
浅谈Python爬虫基本套路
Mar 25 #Python
我用Python抓取了7000 多本电子书案例详解
Mar 25 #Python
详解python:time模块用法
Mar 25 #Python
You might like
在PHP中使用灵巧的体系结构
2006/10/09 PHP
一个颜色轮换的简单例子
2006/10/09 PHP
laravel-admin自动生成模块,及相关基础配置方法
2019/10/08 PHP
Ajax一统天下之Dojo整合篇
2007/03/24 Javascript
Javascript中的数学函数
2007/04/04 Javascript
jQuery MD5加密实现代码
2010/03/15 Javascript
js报错 Object doesn't support this property or method的原因分析
2011/03/31 Javascript
基于jquery的不规则矩形的排列实现代码
2012/04/16 Javascript
深入理解JavaScript是如何实现继承的
2013/12/12 Javascript
JavaScript中变量声明有var和没var的区别示例介绍
2014/09/15 Javascript
Dojo Javascript 编程规范 规范自己的JavaScript书写
2014/10/26 Javascript
JavaScript 学习笔记之数据类型
2015/01/14 Javascript
JS判断是否360安全浏览器极速内核的方法
2015/01/29 Javascript
JS中判断字符串中出现次数最多的字符及出现的次数的简单实例
2016/06/03 Javascript
第一次动手实现bootstrap table分页效果
2016/09/22 Javascript
快速实现jQuery多级菜单效果
2017/02/01 Javascript
基于Vue2实现的仿手机QQ单页面应用功能(接入聊天机器人 )
2017/03/30 Javascript
原生JavaScript实现精美的淘宝轮播图效果示例【附demo源码下载】
2017/05/27 Javascript
react路由配置方式详解
2017/08/07 Javascript
原生javascript自定义input[type=radio]效果示例
2019/08/27 Javascript
基于PHP pthreads实现多线程代码实例
2020/06/24 Javascript
python爬虫的工作原理
2017/03/05 Python
深入理解Django中内置的用户认证
2017/10/06 Python
python 搭建简单的http server,可直接post文件的实例
2019/01/03 Python
python实现证件照换底功能
2019/08/20 Python
自学python用什么系统好
2020/06/23 Python
Python 使用office365邮箱的示例
2020/10/29 Python
统计员岗位职责
2013/11/14 职场文书
音乐兴趣小组活动总结
2014/07/07 职场文书
党员四风自我剖析材料思想汇报
2014/09/13 职场文书
2014年法务工作总结
2014/12/11 职场文书
银行资信证明
2015/06/17 职场文书
水浒传读书笔记
2015/06/25 职场文书
python OpenCV学习笔记
2021/03/31 Python
JS代码编译器Monaco使用方法
2021/06/11 Javascript
VUE使用draggable实现组件拖拽
2022/04/06 Vue.js