深入探究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 相关文章推荐
Java多线程编程中ThreadLocal类的用法及深入
Jun 21 Python
Python入门_浅谈数据结构的4种基本类型
May 16 Python
对python自动生成接口测试的示例讲解
Nov 30 Python
Pycharm如何打断点的方法步骤
Jun 13 Python
Python 的AES加密与解密实现
Jul 09 Python
python爬虫爬取幽默笑话网站
Oct 24 Python
python数据库操作mysql:pymysql、sqlalchemy常见用法详解
Mar 30 Python
使用jupyter Nodebook查看函数或方法的参数以及使用情况
Apr 14 Python
TensorFlow keras卷积神经网络 添加L2正则化方式
May 22 Python
细说NumPy数组的四种乘法的使用
Dec 18 Python
python如何读取.mtx文件
Apr 22 Python
ubuntu安装jupyter并设置远程访问的实现
Mar 31 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中一些可能会被忽略的问题
2013/06/21 PHP
PHP面相对象中的重载与重写
2017/02/13 PHP
基于jQuery的的一个隔行变色,鼠标移动变色的小插件
2010/07/06 Javascript
js原生态函数中使用jQuery中的 $(this)无效的解决方法
2011/05/25 Javascript
页面右下角弹出提示框示例代码js版
2013/08/02 Javascript
js实现动画特效的文字链接鼠标悬停提示的方法
2015/03/02 Javascript
JQuery实现样式设置、追加、移除与切换的方法
2015/06/11 Javascript
jquery实现页面虚拟键盘特效
2015/08/08 Javascript
详解XMLHttpRequest(二)响应属性、二进制数据、监测上传下载进度
2016/09/14 Javascript
Webpack实现按需打包Lodash的几种方法详解
2017/05/08 Javascript
vue指令之表单控件绑定v-model v-model与v-bind结合使用
2019/04/17 Javascript
详解Node.js使用token进行认证的简单示例
2020/05/25 Javascript
Python中使用partial改变方法默认参数实例
2015/04/28 Python
解决PyCharm中光标变粗的问题
2017/08/05 Python
python批量修改图片大小的方法
2018/07/24 Python
Python函数中不定长参数的写法
2019/02/13 Python
pandas修改DataFrame列名的实现方法
2019/02/22 Python
python开启debug模式的方法
2019/06/27 Python
python列表,字典,元组简单用法示例
2019/07/11 Python
python numpy之np.random的随机数函数使用介绍
2019/10/06 Python
Python sys模块常用方法解析
2020/02/20 Python
css3 边框、背景、文本效果的实现代码
2018/03/21 HTML / CSS
html5的画布canvas——画出弧线、旋转的图形实例代码+效果图
2013/06/09 HTML / CSS
澳大利亚设计的优质鞋类和适合澳大利亚生活方式的服装:Rivers
2019/04/23 全球购物
牛津在线药房:Oxford Online Pharmacy
2020/11/16 全球购物
澳大利亚电商Catch新西兰站:Catch.co.nz
2020/05/30 全球购物
如何设定的weblogic的热启动模式(开发模式)与产品发布模式
2012/09/08 面试题
介绍一下结构化程序设计方法和面向对象程序设计方法的区别
2012/06/27 面试题
军训生自我鉴定范文
2013/12/27 职场文书
教师党性分析材料
2014/02/04 职场文书
出生公证委托书
2014/04/03 职场文书
电子商务助理求职自荐信
2014/04/10 职场文书
个人简历自我评价怎么写
2015/03/10 职场文书
合作意向书怎么写
2019/06/24 职场文书
使用pandas模块实现数据的标准化操作
2021/05/14 Python
RPM包方式安装Oracle21c的方法详解
2021/08/23 Oracle