深入探究Python中变量的拷贝和作用域问题


Posted in Python onMay 05, 2015

在 python 中赋值语句总是建立对象的引用值,而不是复制对象。因此,python 变量更像是指针,而不是数据存储区域,

深入探究Python中变量的拷贝和作用域问题

 这点和大多数 OO 语言类似吧,比如 C++、java 等 ~
1、先来看个问题吧:

在Python中,令values=[0,1,2];values[1]=values,为何结果是[0,[...],2]?
 

>>> values = [0, 1, 2]
>>> values[1] = values
>>> values
[0, [...], 2]

我预想应当是

[0, [0, 1, 2], 2]

但结果却为何要赋值无限次?

可以说 Python 没有赋值,只有引用。你这样相当于创建了一个引用自身的结构,所以导致了无限循环。为了理解这个问题,有个基本概念需要搞清楚。

Python 没有「变量」,我们平时所说的变量其实只是「标签」,是引用。

执行

values = [0, 1, 2]

的时候,Python 做的事情是首先创建一个列表对象 [0, 1, 2],然后给它贴上名为 values 的标签。如果随后又执行

values = [3, 4, 5]

的话,Python 做的事情是创建另一个列表对象 [3, 4, 5],然后把刚才那张名为 values 的标签从前面的 [0, 1, 2] 对象上撕下来,重新贴到 [3, 4, 5] 这个对象上。

至始至终,并没有一个叫做 values 的列表对象容器存在,Python 也没有把任何对象的值复制进 values 去。过程如图所示: 

深入探究Python中变量的拷贝和作用域问题

 执行

values[1] = values

的时候,Python 做的事情则是把 values 这个标签所引用的列表对象的第二个元素指向 values 所引用的列表对象本身。执行完毕后,values 标签还是指向原来那个对象,只不过那个对象的结构发生了变化,从之前的列表 [0, 1, 2] 变成了 [0, ?, 2],而这个 ? 则是指向那个对象本身的一个引用。如图所示:

深入探究Python中变量的拷贝和作用域问题

要达到你所需要的效果,即得到 [0, [0, 1, 2], 2] 这个对象,你不能直接将 values[1] 指向 values 引用的对象本身,而是需要吧 [0, 1, 2] 这个对象「复制」一遍,得到一个新对象,再将 values[1] 指向这个复制后的对象。Python 里面复制对象的操作因对象类型而异,复制列表 values 的操作是

values[:] #生成对象的拷贝或者是复制序列,不再是引用和共享变量,但此法只能顶层复制

所以你需要执行

values[1] = values[:]

Python 做的事情是,先 dereference 得到 values 所指向的对象 [0, 1, 2],然后执行 [0, 1, 2][:] 复制操作得到一个新的对象,内容也是 [0, 1, 2],然后将 values 所指向的列表对象的第二个元素指向这个复制二来的列表对象,最终 values 指向的对象是 [0, [0, 1, 2], 2]。过程如图所示: 

深入探究Python中变量的拷贝和作用域问题

 往更深处说,values[:] 复制操作是所谓的「浅复制」(shallow copy),当列表对象有嵌套的时候也会产生出乎意料的错误,比如

a = [0, [1, 2], 3]
b = a[:]
a[0] = 8
a[1][1] = 9

问:此时 a 和 b 分别是多少?

正确答案是 a 为 [8, [1, 9], 3],b 为 [0, [1, 9], 3]。发现没?b 的第二个元素也被改变了。想想是为什么?不明白的话看下图 

深入探究Python中变量的拷贝和作用域问题

 正确的复制嵌套元素的方法是进行「深复制」(deep copy),方法是

 

import copy
 
a = [0, [1, 2], 3]
b = copy.deepcopy(a)
a[0] = 8
a[1][1] = 9

深入探究Python中变量的拷贝和作用域问题

 2、引用 VS 拷贝:

(1)没有限制条件的分片表达式(L[:])能够复制序列,但此法只能浅层复制。

(2)字典 copy 方法,D.copy() 能够复制字典,但此法只能浅层复制

(3)有些内置函数,例如 list,能够生成拷贝 list(L)

(4)copy 标准库模块能够生成完整拷贝:deepcopy 本质上是递归 copy

(5)对于不可变对象和可变对象来说,浅复制都是复制的引用,只是因为复制不变对象和复制不变对象的引用是等效的(因为对象不可变,当改变时会新建对象重新赋值)。所以看起来浅复制只复制不可变对象(整数,实数,字符串等),对于可变对象,浅复制其实是创建了一个对于该对象的引用,也就是说只是给同一个对象贴上了另一个标签而已。
 

L = [1, 2, 3]
D = {'a':1, 'b':2}
A = L[:]
B = D.copy()
print "L, D"
print L, D
print "A, B"
print A, B
print "--------------------"
A[1] = 'NI'
B['c'] = 'spam'
print "L, D"
print L, D
print "A, B"
print A, B
 
 
L, D
[1, 2, 3] {'a': 1, 'b': 2}
A, B
[1, 2, 3] {'a': 1, 'b': 2}
--------------------
L, D
[1, 2, 3] {'a': 1, 'b': 2}
A, B
[1, 'NI', 3] {'a': 1, 'c': 'spam', 'b': 2}

3、增强赋值以及共享引用:

x = x + y,x 出现两次,必须执行两次,性能不好,合并必须新建对象 x,然后复制两个列表合并

属于复制/拷贝

x += y,x 只出现一次,也只会计算一次,性能好,不生成新对象,只在内存块末尾增加元素。

当 x、y 为list时, += 会自动调用 extend 方法进行合并运算,in-place change。

属于共享引用
 

L = [1, 2]
M = L
L = L + [3, 4]
print L, M
print "-------------------"
L = [1, 2]
M = L
L += [3, 4]
print L, M
 
 
[1, 2, 3, 4] [1, 2]
-------------------
[1, 2, 3, 4] [1, 2, 3, 4]

4、python 从2.x 到3.x,语句变函数引发的变量作用域问题 

先看段代码:
 

def test():
  a = False
  exec ("a = True")
  print ("a = ", a)
test()
 
b = False
exec ("b = True")
print ("b = ", b)

在 python 2.x 和 3.x 下 你会发现他们的结果不一样:
 

2.x:
a = True
b = True
 
3.x:
a = False
b = True

这是为什么呢?

因为 3.x 中 exec 由语句变成函数了,而在函数中变量默认都是局部的,也就是说

你所见到的两个 a,是两个不同的变量,分别处于不同的命名空间中,而不会冲突。

具体参考 《learning python》P331-P332

知道原因了,我们可以这么改改:
 

def test():
  a = False
  ldict = locals()
  exec("a=True",globals(),ldict)
  a = ldict['a']
  print(a)
 
test()
 
b = False
exec("b = True", globals())
print("b = ", b)

这个问题在  stackoverflow 上已经有人问了,而且 python 官方也有人报了 bug。。。

具体链接在下面:

http://stackoverflow.com/questions/7668724/variables-declared-in-execed-code-dont-become-local-in-python-3-documentatio

http://bugs.python.org/issue4831

http://stackoverflow.com/questions/1463306/how-does-exec-work-with-locals

Python 相关文章推荐
使用python编写批量卸载手机中安装的android应用脚本
Jul 21 Python
Python栈类实例分析
Jun 15 Python
python实现各进制转换的总结大全
Jun 18 Python
python如何将图片转换为字符图片
Aug 19 Python
使用Eclipse如何开发python脚本
Apr 11 Python
Python基于jieba库进行简单分词及词云功能实现方法
Jun 16 Python
Python常用模块之requests模块用法分析
May 15 Python
python设置环境变量的原因和方法
Jun 24 Python
Python Numpy计算各类距离的方法
Jul 05 Python
python在地图上画比例的实例详解
Nov 13 Python
接口自动化多层嵌套json数据处理代码实例
Nov 20 Python
Python 的 sum() Pythonic 的求和方法详细
Oct 16 Python
Python使用metaclass实现Singleton模式的方法
May 05 #Python
python中查看变量内存地址的方法
May 05 #Python
Python中统计函数运行耗时的方法
May 05 #Python
Python调用命令行进度条的方法
May 05 #Python
Python记录详细调用堆栈日志的方法
May 05 #Python
进一步探究Python的装饰器的运用
May 05 #Python
Python获取任意xml节点值的方法
May 05 #Python
You might like
利用PHP创建动态图像
2006/10/09 PHP
MayFish PHP的MVC架构的开发框架
2009/08/13 PHP
PHP Session变量不能传送到下一页的解决方法
2009/11/27 PHP
PHP中SSO Cookie登录分析和实现
2015/11/06 PHP
PHP编写RESTful接口的方法
2016/02/21 PHP
php删除txt文件指定行及按行读取txt文档数据的方法
2017/01/30 PHP
PHP自动补全表单的两种方法
2017/03/06 PHP
Laravel中encrypt和decrypt的实现方法
2017/09/24 PHP
JavaScript替换当前页面的方法
2015/04/03 Javascript
jQuery多级手风琴菜单实例讲解
2015/10/22 Javascript
浅析JavaScript作用域链、执行上下文与闭包
2016/02/01 Javascript
jQuery实现的纵向下拉菜单实例详解【附demo源码下载】
2016/07/09 Javascript
js滚轮事件兼容性问题需要注意哪些
2016/11/15 Javascript
常用的javascript设计模式
2017/01/11 Javascript
JS+canvas画布实现炫酷的旋转星空效果示例
2019/02/13 Javascript
layui switch 开关监听 弹出确定状态转换的例子
2019/09/21 Javascript
微信小程序个人中心的列表控件实现代码
2020/04/26 Javascript
js实现简单的无缝轮播效果
2020/09/05 Javascript
vue 使用lodash实现对象数组深拷贝操作
2020/09/10 Javascript
仅用500行Python代码实现一个英文解析器的教程
2015/04/02 Python
python保存字符串到文件的方法
2015/07/01 Python
flask-restful使用总结
2018/12/04 Python
在Python中关于使用os模块遍历目录的实现方法
2019/01/03 Python
python 将大文件切分为多个小文件的实例
2019/01/14 Python
Python给图像添加噪声具体操作
2019/03/03 Python
python自制包并用pip免提交到pypi仅安装到本机【推荐】
2019/06/03 Python
使用python实现ftp的文件读写方法
2019/07/02 Python
详解python os.path.exists判断文件或文件夹是否存在
2020/11/16 Python
史上最详细的Python打包成exe文件教程
2021/01/17 Python
python爬虫实现爬取同一个网站的多页数据的实例讲解
2021/01/18 Python
html5的pushstate以及监听浏览器返回事件的实现
2020/08/11 HTML / CSS
个人先进事迹材料
2014/12/29 职场文书
80后创业总结的9条职场用人思想,记得收藏
2019/08/13 职场文书
Html5通过数据流方式播放视频的实现
2021/04/27 HTML / CSS
Python源码解析之List
2021/05/21 Python
Win11怎么启动任务管理器?Win11启动任务管理器的几种方法
2021/11/23 数码科技