1. 程式人生 > >python 深入理解 賦值、引用、拷貝、作用域

python 深入理解 賦值、引用、拷貝、作用域

似的 list 參數傳遞 question net 做的 標準 理解 官方

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 去。過程如圖所示:
技術分享圖片

執行

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

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

具體鏈接在下面:

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

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

python 深入理解 賦值、引用、拷貝、作用域