深入理解 python 中的賦值、引用、拷貝

深入理解 python 中的賦值、引用、拷貝

python基礎(5):深入理解 python 中的賦值、引用、拷貝、作用域 原

大數據之路 發佈於 2013/07/21 01:14

在 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 去。過程如圖所示:

執行

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

2、引用 VS 拷貝:

(1)沒有限制條件的分片表達式(L[:])能夠複製序列,但此法只能淺層複製。

(2)字典 copy 方法,D.copy() 能夠複製字典,但此法只能淺層複製

(3)有些內置函數,例如 list,能夠生成拷貝 list(L)

(4)copy 標準庫模塊能夠生成完整拷貝:deepcopy 本質上是遞歸 copy,複製文件,包含子文件,遞歸概念,A包含B,B包含C,則A包含C

(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 從 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。。。

具體鏈接在下面:

stackoverflow.com/quest

bugs.python.org/issue48

stackoverflow.com/quest

這是一個典型的 python 2k 移植到 3k 不兼容的案例,類似的還有很多,也算是移植的坑吧~

具體的 2k 與 3k 有哪些差異可以看這裡:

使用 2to3 將代碼移植到 Python 3

woodpecker.org.cn/divei

5、深入理解 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在函數調用的每一次都被反覆引用。這可能有一些意想不到的後果。


推薦閱讀:

TAG:Python | 編程語言 | Python入門 |