python基础(5):深刻理解 python 中的赋值、引用、拷贝、做用域

在 python 中赋值语句老是创建对象的引用值,而不是复制对象。所以,python 变量更像是指针,而不是数据存储区域,html

这点和大多数 OO 语言相似吧,好比 C++、java 等 ~java

一、先来看个问题吧:

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

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

我预想应当是 git

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

但结果却为什么要赋值无限次?程序员

 

能够说 Python 没有赋值,只有引用。你这样至关于建立了一个引用自身的结构,因此致使了无限循环。为了理解这个问题,有个基本概念须要搞清楚。

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

执行 segmentfault

values = [0, 1, 2]

的时候,Python 作的事情是首先建立一个列表对象 [0, 1, 2],而后给它贴上名为 values 的标签。若是随后又执行app

values = [3, 4, 5]

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

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

执行性能

values[1] = values

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

要达到你所须要的效果,即获得 [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]。过程如图所示:

往更深处说,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 的第二个元素也被改变了。想一想是为何?不明白的话看下图

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

 

import copy

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

二、引用 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}

三、加强赋值以及共享引用:

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]

四、python 从 2k 到 3k,语句变函数引起的变量做用域问题  

先看段代码:

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

b = False
exec ("b = True")
print ("b = ", b)

在 python 2k 和 3k 下 你会发现他们的结果不同:

2K:
a =  True
b =  True

3K:
a =  False
b =  True

这是为何呢?

由于 3k 中 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 2k 移植到 3k 不兼容的案例,相似的还有不少,也算是移植的坑吧~

具体的 2k 与 3k 有哪些差别能够看这里:

使用 2to3 将代码移植到 Python 3

http://woodpecker.org.cn/diveintopython3/porting-code-to-python-3-with-2to3.html

五、深刻理解 python 变量做用域及其陷阱

5.1 可变对象 & 不可变对象

在Python中,对象分为两种:可变对象和不可变对象,不可变对象包括int,float,long,str,tuple等,可变对象包括list,set,dict等。须要注意的是:这里说的不可变指的是值的不可变。对于不可变类型的变量,若是要更改变量,则会建立一个新值,把变量绑定到新值上,而旧值若是没有被引用就等待垃圾回收。另外,不可变的类型能够计算hash值,做为字典的key。可变类型数据对对象操做的时候,不须要再在其余地方申请内存,只须要在此对象后面连续申请(+/-)便可,也就是它的内存地址会保持不变,但区域会变长或者变短。

>>> a = 'xianglong.me'
>>> id(a)
140443303134352
>>> a = '1saying.com'
>>> id(a)
140443303131776
# 从新赋值以后,变量a的内存地址已经变了
# 'xianglong.me'是str类型,不可变,因此赋值操做知识从新建立了str '1saying.com'对象,而后将变量a指向了它
 
>>> a_list = [1, 2, 3]
>>> id(a_list)
140443302951680
>>> a_list.append(4)
>>> id(a_list)
140443302951680
# list从新赋值以后,变量a_list的内存地址并未改变
# [1, 2, 3]是可变的,append操做只是改变了其value,变量a_list指向没有变

5.2 函数值传递

def func_int(a):
    a += 4
 
def func_list(a_list):
    a_list[0] = 4
 
t = 0
func_int(t)
print t
# output: 0
 
t_list = [1, 2, 3]
func_list(t_list)
print t_list
# output: [4, 2, 3]

 

 对于上面的输出,很多Python初学者都比较疑惑:第一个例子看起来像是传值,而第二个例子确实传引用。其实,解释这个问题也很是容易,主要是由于可变对象和不可变对象的缘由:对于可变对象,对象的操做不会重建对象,而对于不可变对象,每一次操做就重建新的对象。

    在函数参数传递的时候,Python其实就是把参数里传入的变量对应的对象的引用依次赋值给对应的函数内部变量。参照上面的例子来讲明更容易理解,func_int中的局部变量"a"实际上是所有变量"t"所指向对象的另外一个引用,因为整数对象是不可变的,因此当func_int对变量"a"进行修改的时候,其实是将局部变量"a"指向到了整数对象"1"。因此很明显,func_list修改的是一个可变的对象,局部变量"a"和全局变量"t_list"指向的仍是同一个对象。

5.3 为何修改全局的dict变量不用global关键字

为何修改字典d的值不用global关键字先声明呢?

s = 'foo'
d = {'a':1}
def f():
    s = 'bar'
    d['b'] = 2
f()
print s  # foo
print d  # {'a': 1, 'b': 2}

这是由于,在s = 'bar'这句中,它是“有歧义的“,由于它既能够是表示引用全局变量s,也能够是建立一个新的局部变量,因此在python中,默认它的行为是建立局部变量,除非显式声明global,global定义的本地变量会变成其对应全局变量的一个别名,便是同一个变量。

在d['b']=2这句中,它是“明确的”,由于若是把d看成是局部变量的话,它会报KeyError,因此它只能是引用全局的d,故不须要画蛇添足显式声明global。

上面这两句赋值语句实际上是不一样的行为,一个是rebinding(不可变对象), 一个是mutation(可变对象).

可是若是是下面这样:

d = {'a':1}
def f():
    d = {}
    d['b'] = 2
f()
print d  # {'a': 1}

 

在d = {}这句,它是”有歧义的“了,因此它是建立了局部变量d,而不是引用全局变量d,因此d['b']=2也是操做的局部变量。

推而远之,这一切现象的本质就是”它是不是明确的“。

仔细想一想,就会发现不止dict不须要global,全部”明确的“东西都不须要global。由于int类型str类型之类的不可变对象,每一次操做就重建新的对象,他们只有一种修改方法,即x = y, 刚好这种修改方法同时也是建立变量的方法,因此产生了歧义,不知道是要修改仍是建立。而dict/list/对象等可变对象,操做不会重建对象,能够经过dict['x']=y或list.append()之类的来修改,跟建立变量不冲突,不产生歧义,因此都不用显式global。

5.4 可变对象 list 的 = 和 append/extend 差异在哪?

接上面 5.3 的理论,下面我们再看一例常见的错误:

# coding=utf-8
# 测试utf-8编码
import sys
reload(sys)
sys.setdefaultencoding('utf-8')

list_a = []
def a():
    list_a = [1]      ## 语句1
a()
print list_a    # []

print "======================"

list_b = []
def b():
    list_b.append(1)    ## 语句2
b()
print list_b    # [1]

你们能够看到为何 语句1 不能改变 list_a 的值,而 语句2 却能够?他们的差异在哪呢?

由于 = 建立了局部变量,而 .append() 或者 .extend() 重用了全局变量。

5.5 陷阱:使用可变的默认参数

我屡次见到过以下的代码:

def foo(a, b, c=[]):
# append to c
# do some more stuff

永远不要使用可变的默认参数,可使用以下的代码代替:

def foo(a, b, c=None):
    if c is None:
        c = []
    # append to c
    # do some more stuff

‍与其解释这个问题是什么,不如展现下使用可变默认参数的影响:‍

In[2]: def foo(a, b, c=[]):
...        c.append(a)
...        c.append(b)
...        print(c)
...
In[3]: foo(1, 1)
[1, 1]
In[4]: foo(1, 1)
[1, 1, 1, 1]
In[5]: foo(1, 1)
[1, 1, 1, 1, 1, 1]

同一个变量c在函数调用的每一次都被反复引用。这可能有一些意想不到的后果。

REF:

[1] 《learning python》:P130、P13四、P20二、P204 、P245

http://www.zhihu.com/question/21000872/answer/16856382

[2] 理解 Python 的 LEGB

http://blog.segmentfault.com/sunisdown/1190000000640834

[3] Python函数参数默认值的陷阱和原理深究

http://cenalulu.github.io/python/default-mutable-arguments/

[4] 潜在的Python陷阱

http://python.jobbole.com/81564/

[5] 陷阱!python参数默认值

http://segmentfault.com/a/1190000000743526

[6] Python中的变量、引用、拷贝和做用域

http://xianglong.me/article/python-variable-quote-copy-and-scope/

[7] Python入门基础知识(1) :locals() 和globals()

http://www.cnblogs.com/wanxsb/archive/2013/05/07/3064783.html

[8] Python程序员写代码时应该避免的16个“坑”

http://bit.ly/29vnLvz

相关文章
相关标签/搜索