详解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类参数self使用示例
Feb 17 Python
Python中bisect的用法
Sep 23 Python
Python中暂存上传图片的方法
Feb 18 Python
celery4+django2定时任务的实现代码
Dec 23 Python
python交互界面的退出方法
Feb 16 Python
Python2.7版os.path.isdir中文路径返回false的解决方法
Jun 21 Python
Django调用百度AI接口实现人脸注册登录代码实例
Apr 23 Python
pytorch加载自己的图像数据集实例
Jul 07 Python
使用tensorflow进行音乐类型的分类
Aug 14 Python
python温度转换华氏温度实现代码
Dec 06 Python
详解解决jupyter不能使用pytorch的问题
Feb 18 Python
详解Django中 render() 函数的使用方法
Apr 22 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日期处理函数 整型日期格式
2011/01/12 PHP
php读取mysql中文数据出现乱码的解决方法
2013/08/16 PHP
mantis安装、配置和使用中的问题小结
2014/07/14 PHP
收藏Javascript中常用的55个经典技巧
2007/08/12 Javascript
一起来写段JS drag拖动代码
2010/12/09 Javascript
js数组Array sort方法使用深入分析
2013/02/21 Javascript
jQuery下的动画处理总结
2013/10/10 Javascript
IE下支持文本框和密码框placeholder效果的JQuery插件分享
2015/01/31 Javascript
jquery实现用户打分评分特效
2015/05/28 Javascript
javascript常用的方法分享
2015/07/01 Javascript
JS实现仿微博可关闭弹出层效果
2015/09/21 Javascript
Node.js静态文件服务器改进版
2016/01/10 Javascript
精彩的Bootstrap案例分享 重点在注释!(选项卡、栅格布局)
2016/07/01 Javascript
jQuery实现Select左右复制移动内容
2016/08/05 Javascript
Angular的$http的ajax的请求操作(推荐)
2017/01/10 Javascript
Easyui笔记2:实现datagrid多行删除的示例代码
2017/01/14 Javascript
利用vue组件自定义v-model实现一个Tab组件方法示例
2017/12/06 Javascript
layui框架与SSM前后台交互的方法
2019/09/12 Javascript
浅谈vuex为什么不建议在action中修改state
2020/02/02 Javascript
Vue组件间的通信pubsub-js实现步骤解析
2020/03/11 Javascript
Python利用Beautiful Soup模块创建对象详解
2017/03/27 Python
Python数据处理numpy.median的实例讲解
2018/04/02 Python
Python检查ping终端的方法
2019/01/26 Python
python hough变换检测直线的实现方法
2019/07/12 Python
python lambda表达式在sort函数中的使用详解
2019/08/28 Python
你还在@微信官方?聊聊Python生成你想要的微信头像
2019/09/25 Python
python matplotlib 画dataframe的时间序列图实例
2019/11/20 Python
QML实现钟表效果
2020/06/02 Python
如何基于Python pygame实现动画跑马灯
2020/11/18 Python
cookies应对python反爬虫知识点详解
2020/11/25 Python
使用css如何制作时间ICON方法实践
2012/11/12 HTML / CSS
美国东北部户外服装和设备零售商:Eastern Mountain Sports
2016/10/05 全球购物
关于责任的演讲稿
2014/05/20 职场文书
法制教育演讲稿
2014/09/10 职场文书
MySQL库表太大怎么办? 数据库分库分表项目实践
2022/04/11 MySQL
SpringBoot Http远程调用的方法
2022/08/14 Java/Android