Python中的动态赋值及其陷阱

本文深入探讨Python中locals()与globals()函数的使用差异,揭示动态赋值的内部机制。locals()虽显示局部命名空间,但仅可读;globals()则可读可写。文章通过源码解析,解释了为何locals()无法修改局部变量。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

在Python当中,命名空间与作用域的问题,看似微不足道,其实背后暗藏玄机。
本文着重介绍Python当中的 locals()globals() 的读写问题,以及由这个问题引申出来的动态赋值相关的问题
locals()globals() 的调用结果都是字典类型,用法在这里我就不再赘述了。然而,在使用过程中,有一个陷阱需要注意:globals() 可读可写,而 locals() 只可读却不可写。今天分享的文章,就是在探究这个问题,写得比较深入,在这里分享给大家。

正文

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

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

例如:

def test():
	globals()['a2'] = 4

test()
print(a2)  # result:4  

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

但事实真是如此吗?答案是否定的!

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

test2()  

输出结果:

{}  
{'i': 'c', 'a': 1, 'c': 1, 'b': 1}  
Traceback (most recent call last):  
...
NameError: global name 'a' is not defined  

可以看到程序运行报错了!

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

再看一个例子:

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

test3()  

输出结果:

{}  
{'i': 'c', 'a': 1, 's': 'test', 'b': 1, 'c': 1}
test
Traceback (most recent call last):
...
NameError: global name 'a' is not defined  

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

这就让广大的猿儿们一脸懵逼了,难道通过 locals() 改变局部变量,和直接赋值产生的结果不一样?那么想要解决这个问题,只能去看程序运行的真相了。

根源探讨

我们来拆解Python源码:

import dis  
dis.dis(test3)  

然后直接对第二段代码解析:
111
在上面的字节码中我们可以看到:

  1. locals() 对应的字节码是:LOAD_GLOBAL
  2. s='test'对应的字节码是:LOAD_CONSTSTORE_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_localsplus0 个成员取出来,也就是值 “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. 分别将 localsplusco_cellvarsco_freevars 写入 f_f- >f_locals

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

  • localsplus:函数参数(位置参数+关键字参数),显式赋值的变量。
  • co_cellvarsco_freevars :闭包函数会用到的局部变量。

结论

通过上面的源码结论已经清晰可见, 我们通过调用locals() 看到的结果,的确是函数的局部命名空间的内容,但是这个结果不能代表局部命名空间,只有在函数中被显式赋值的变量,以及闭包函数中会用到的局部变量才是‘正规军’,通过 locals()[i] = 1 这种方式动态赋的值属于‘杂牌军’,在这里没有话语权。而 globals() 却不一样,两种方式都拥有足够的话语权。那么最后总结为一句话:
locals() 只读, globals() 可读可写!

注:感谢Python猫的倾情分享!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值