详解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调用短信猫控件实现发短信功能实例
Jul 04 Python
python中的字典详细介绍
Sep 18 Python
python中将字典形式的数据循环插入Excel
Jan 16 Python
单利模式及python实现方式详解
Mar 20 Python
Python基于socket模块实现UDP通信功能示例
Apr 10 Python
python 2.7.13 安装配置方法图文教程
Sep 18 Python
Flask配置Cors跨域的实现
Jul 12 Python
Python简易版图书管理系统
Aug 12 Python
浅谈selenium如何应对网页内容需要鼠标滚动加载的问题
Mar 14 Python
python图片指定区域替换img.paste函数的使用
Apr 09 Python
python链表类中获取元素实例方法
Feb 23 Python
Python NumPy灰度图像的压缩原理讲解
Aug 04 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 Memcached应用实现代码
2010/02/08 PHP
强制PHP命令行脚本单进程运行的方法
2014/04/15 PHP
基于PHP代码实现中奖概率算法可用于刮刮卡、大转盘等抽奖算法
2015/12/20 PHP
Laravel接收前端ajax传来的数据的实例代码
2017/07/20 PHP
jquery刷新页面的实现代码(局部及全页面刷新)
2011/07/11 Javascript
javascript学习笔记(五)原型和原型链详解
2014/10/08 Javascript
手机端图片缩放旋转全屏查看PhotoSwipe.js插件实现
2016/08/25 Javascript
bootstrap响应式表格实例详解
2017/05/15 Javascript
ES6学习教程之对象字面量详解
2017/10/09 Javascript
jQuery实现的简单手风琴效果示例
2018/08/29 jQuery
vue-cli 使用vue-bus来全局控制的实例讲解
2018/09/15 Javascript
Jquery $.map使用方法实例详解
2020/09/01 jQuery
Vue + ts实现轮播插件的示例
2020/11/10 Javascript
vue 动态添加的路由页面刷新时失效的原因及解决方案
2021/02/26 Vue.js
[50:58]2018DOTA2亚洲邀请赛 4.1 小组赛 B组 Mineski vs EG
2018/04/03 DOTA
Python的字典和列表的使用中一些需要注意的地方
2015/04/24 Python
python抓取并保存html页面时乱码问题的解决方法
2016/07/01 Python
浅析PHP与Python进行数据交互
2018/05/15 Python
详解Django中类视图使用装饰器的方式
2018/08/12 Python
mac PyCharm添加Python解释器及添加package路径的方法
2018/10/29 Python
Django框架反向解析操作详解
2019/11/28 Python
python with (as)语句实例详解
2020/02/04 Python
Python urllib request模块发送请求实现过程解析
2020/12/10 Python
德国狗狗用品在线商店:Schecker
2017/03/17 全球购物
五年级语文教学反思
2014/01/30 职场文书
初中优秀班集体申报材料
2014/05/01 职场文书
经典演讲稿汇总
2014/05/19 职场文书
酒店管理专业自荐信
2014/05/23 职场文书
一个都不能少观后感
2015/06/04 职场文书
数学备课组工作总结
2015/08/12 职场文书
大学军训心得体会800字
2016/01/11 职场文书
golang正则之命名分组方式
2021/04/25 Golang
Python3.8官网文档之类的基础语法阅读
2021/09/04 Python
Go语言特点及基本数据类型使用详解
2022/03/21 Golang
引用计数法和root搜索算法以及JVM中判定对象需要回收的方法
2022/04/19 Java/Android
vue二维数组循环嵌套方式 循环数组、循环嵌套数组
2022/04/24 Vue.js