深入探究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使用datetime模块计算各种时间间隔的方法
Mar 24 Python
Python读写docx文件的方法
May 08 Python
python模块导入的细节详解
Dec 10 Python
python矩阵的转置和逆转实例
Dec 12 Python
python 多线程重启方法
Feb 18 Python
详解Python读取yaml文件多层菜单
Mar 23 Python
解决pycharm最左侧Tool Buttons显示不全的问题
Dec 17 Python
python 实现从高分辨图像上抠取图像块
Jan 02 Python
Python tkinter 下拉日历控件代码
Mar 04 Python
Python使用jpype模块调用jar包过程解析
Jul 29 Python
python3环境搭建过程(利用Anaconda+pycharm)完整版
Aug 19 Python
Python 如何利用ffmpeg 处理视频素材
Nov 27 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与MySQL开发中页面乱码的产生与解决
2008/03/27 PHP
如何用php获取文件名后缀
2013/06/09 PHP
PHP弹出对话框技巧详细解读
2015/09/26 PHP
thinkphp命名空间用法实例详解
2015/12/30 PHP
Laravel5.* 打印出执行的sql语句的方法
2017/07/24 PHP
浅析PHP开发规范
2018/02/05 PHP
Laravel框架文件上传功能实现方法示例
2019/04/16 PHP
formvalidator验证插件中有关ajax验证问题
2013/01/04 Javascript
JS模块与命名空间的介绍
2013/03/22 Javascript
通过js简单实现将一个文本内容转译成加密文本
2013/10/22 Javascript
探讨JQUERY JSON的反序列化类 using问题的解决方法
2013/12/19 Javascript
php利用curl获取远程图片实现方法
2015/10/26 Javascript
浅谈javascript控制HTML5的全屏操控,浏览器兼容的问题
2016/10/10 Javascript
详解angularJs模块ui-router之状态嵌套和视图嵌套
2017/04/28 Javascript
bootstrap实现点击删除按钮弹出确认框的实例代码
2018/08/16 Javascript
加快Vue项目的开发速度的方法
2018/12/12 Javascript
npm qs模块使用详解
2020/02/07 Javascript
vue实现导航菜单和编辑文本的示例代码
2020/07/04 Javascript
Vue props中Object和Array设置默认值操作
2020/07/30 Javascript
Python中的高级数据结构详解
2015/03/27 Python
用Python编写生成树状结构的文件目录的脚本的教程
2015/05/04 Python
python中异常报错处理方法汇总
2016/11/20 Python
使用paramiko远程执行命令、下发文件的实例
2017/10/01 Python
pandas 快速处理 date_time 日期格式方法
2018/11/12 Python
python版大富翁源代码分享
2018/11/19 Python
详解分布式任务队列Celery使用说明
2018/11/29 Python
python+numpy按行求一个二维数组的最大值方法
2019/07/09 Python
Python3环境安装Scrapy爬虫框架过程及常见错误
2019/07/12 Python
tensorflow入门:TFRecordDataset变长数据的batch读取详解
2020/01/20 Python
公认8个效率最高的爬虫框架
2020/07/28 Python
美国优质宠物用品购买网站:Muttropolis
2020/02/17 全球购物
党支部创先争优活动总结
2014/08/28 职场文书
知识就是力量演讲稿
2014/09/13 职场文书
2014年房产销售工作总结
2014/12/08 职场文书
MySQL插入数据与查询数据
2022/03/25 MySQL
win11如何查看端口是否被占用? Win11查看端口是否占用的技巧
2022/04/05 数码科技