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

相關文章
相關標籤/搜索