1. 程式人生 > >【Python】引用和物件的理解

【Python】引用和物件的理解

Overview

在Python中使用變數進行值修改、引數傳遞、以及複製變數等等的過程中,往往會出現一些我們意想不到的“錯誤”。
但實際上產生這些“錯誤”的原因,大多是因為沒有深入地理解Python內部的物件引用機制。
針對於此,筆者大致整理了10個例子,用以循序漸進地幫助大家加深對於Python引用和物件的理解。
Here we go.

Example 1

a = 3

這是一個簡單的賦值語句,整數 3 為一個物件,a 是一個引用,利用賦值語句,引用a指向了物件3。
形象比喻一下:這個過程就相當於“放風箏”,變數a就是你手裡面的“線”,python就跟那根“線”一樣,通過引用來接觸和拴住天空中的風箏——物件。
引用

[3]
你可以通過python的內建函式 id() 來檢視物件的身份(identity),這個所謂的身份其實就是 物件 的記憶體地址。

Example 2

利用上面的 id()函式:

>>> a = 1
>>> id(a)
24834392

>>> a = 'banana'
>>> id(a)
139990659655312

第一個語句中, 2是儲存在記憶體中的一個整數物件,通過賦值 引用a 指向了 物件 1
第二個語句中,記憶體中建立了一個字串物件‘banana’,通過賦值 將 引用a 指向了 ‘banana’,同時,物件1不在有引用指向它,它會被python的記憶體處理機制給當我垃圾回收,釋放記憶體。

Example 3

>>> a = 3
>>> b = 3

>>> id(a)
10289448
>>> id(b)
10289448

在這裡可以看到 這倆個引用 指向了同一個 物件,這是為什麼呢? 這個跟python的記憶體機制有關係,因為對於語言來說,頻繁的進行物件的銷燬和建立,特別浪費效能。所以在Python中,整數和短小的字元,Python都會快取這些物件,以便重複使用。

Example 4

>>> a = 4
>>> b = a
>>> id(a)
36151568
>>> id(b) 36151568 >>> a = a + 2 >>> id(a) 36151520 >>> id(b) 36151568

可以看到 a 的引用改變了,但是 b 的引用未發生改變;a,b指向不同的物件; 第3句對 a 進行了重新賦值,讓它指向了新的 物件6;即使是多個引用指向同一個物件,如果一個引用值發生變化,那麼實際上是讓這個引用指向一個新的引用,並不影響其他的引用的指向。從效果上看,就是各個引用各自獨立,互不影響。

Example 5

>>> L1 = [1,2,3]
>>> L2 = L1
>>> id(L1)
139643051219496
>>> id(L2)
139643051219496

>>> L1[0] = 10
>>> id(L1)
139643051219496
>>> id(L2)
139643051219496
>>> L2
[10, 2, 3]

同樣的跟Example 4那樣,修改了其中一個物件的值,但是可以發現 結果 並不與 Example 4相同, 在本次實驗中,L1 和 L2 的引用沒有發生任何變化,但是 列表物件[1,2,3] 的值 變成了 [10,2,3](列表物件改變了)

在該情況下,我們不再對L1這一引用賦值,而是對L1所指向的表的元素賦值。結果是,L2也同時發生變化。
原因何在呢?因為L1,L2的指向沒有發生變化,依然指向那個表。表實際上是包含了多個引用的物件(每個引用是一個元素,比如L1[0],L1[1]…, 每個引用指向一個物件,比如1,2,3), 。而L1[0] = 10這一賦值操作,並不是改變L1的指向,而是對L1[0], 也就是表物件的一部份(一個元素),進行操作,所以所有指向該物件的引用都受到影響。
(與之形成對比的是,我們之前的賦值操作都沒有對物件自身發生作用,只是改變引用指向。)

列表可以通過引用其元素,改變物件自身(in-place change)。這種物件型別,稱為可變資料物件(mutable object),詞典也是這樣的資料型別。
而像之前的數字和字串,不能改變物件本身,只能改變引用的指向,稱為不可變資料物件(immutable object)。對於元組(tuple),儘管可以呼叫引用元素,但不可以賦值,因此不能改變物件自身,所以也算是immutable object.

Example 6

>>> a = 4 # id(a) = 36151568
>>> b = a # id(b) = 36151568
>>> a is b 
True

>>> a = a + 2 # id(a) = 36151520
>>> a is b # id(b) = 36151568
False

如果我們想知道兩個引用是否指向同一個物件,可以使用 python的 is 關鍵詞,is即是用於判斷兩個引用所指的物件是否相同。

Example 7

def add_list(p):
    p = p + [1]
p1 = [1, 2, 3]
add_list(p1)
print p1
>>> [1, 2, 3]

def add_list(p):
    p += [1]
p2 = [1, 2, 3]
proc2(p2)
print p2
>>>[1, 2, 3, 1]

這主要是由於“=”操作符會新建一個新的變數儲存賦值結果,然後再把引用名指向“=”左邊,即修改了原來的p引用,使p成為指向新賦值變數的引用。而+=不會,直接修改了原來p引用的內容,事實上+=和=在python內部使用了不同的實現函式。

Example 8

a = []
b = {'num': 0, 'sqrt': 0}
resurse = [1, 2, 3]
for i in resurse:
    b['num'] = i
    b['sqrt'] = i * i
    a.append(b)
print a
>>> [{'num': 3, 'sqrt': 9}, {'num': 3, 'sqrt': 9}, {'num': 3, 'sqrt': 9}]

但我們實際想要的結果是這樣的:

>>> [{'num': 1, 'sqrt': 1}, {'num': 2, 'sqrt': 4}, {'num': 3, 'sqrt': 9}]

這是由於a中的元素就是b的引用。可以修改為:

a = []
resurse = [1, 2, 3]
for i in resurse:
    a.append({"num": i, "sqrt": i * i})

Example 9

定義一個家族譜詞典:value為key的parent. 要寫一個函式,輸入人名,給出這個人的所有祖先名字。
開始的做法:

ada_family = { 'Judith Blunt-Lytton': ['Anne Isabella Blunt', 'Wilfrid Scawen Blunt'],
              'Ada King-Milbanke': ['Ralph King-Milbanke', 'Fanny Heriot'],
              'Ralph King-Milbanke': ['Augusta Ada King', 'William King-Noel'],
              'Anne Isabella Blunt': ['Augusta Ada King', 'William King-Noel'],
              'Byron King-Noel': ['Augusta Ada King', 'William King-Noel'],
              'Augusta Ada King': ['Anne Isabella Milbanke', 'George Gordon Byron'],
              'George Gordon Byron': ['Catherine Gordon', 'Captain John Byron'],
              'John Byron': ['Vice-Admiral John Byron', 'Sophia Trevannion'] }


def ancestors(genealogy, person):
    if person in genealogy:
        parents = genealogy[person]
        result = parents
        for parent in parents:
            result += ancestors(genealogy, parent)
        return result
    return []

print ancestors2(ada_family, 'Judith Blunt-Lytton')
print ada_family


>>> ['Anne Isabella Blunt', 'Wilfrid Scawen Blunt', 'Augusta Ada King',
    'William King-Noel', 'Anne Isabella Milbanke', 'George Gordon Byron',
    'Catherine Gordon', 'Captain John Byron', 'Catherine Gordon',
    'Captain John Byron', 'Anne Isabella Milbanke', 'George Gordon Byron',
    'Catherine Gordon', 'Captain John Byron', 'Catherine Gordon',
    'Captain John Byron', 'Catherine Gordon', 'Captain John Byron',
    'Catherine Gordon', 'Captain John Byron']
>>> {'Ralph King-Milbanke': ['Augusta Ada King', 'William King-Noel'],
    'Ada King-Milbanke': ['Ralph King-Milbanke', 'Fanny Heriot'],
    'Anne Isabella Blunt': ['Augusta Ada King', 'William King-Noel', 'Anne Isabella Milbanke', 'George Gordon Byron', 'Catherine Gordon', 'Captain John Byron', 'Catherine Gordon', 'Captain John Byron'],
    'Augusta Ada King': ['Anne Isabella Milbanke', 'George Gordon Byron', 'Catherine Gordon', 'Captain John Byron', 'Catherine Gordon', 'Captain John Byron'],
    'Judith Blunt-Lytton': ['Anne Isabella Blunt', 'Wilfrid Scawen Blunt', 'Augusta Ada King', 'William King-Noel', 'Anne Isabella Milbanke', 'George Gordon Byron', 'Catherine Gordon', 'Captain John Byron', 'Catherine Gordon', 'Captain John Byron', 'Anne Isabella Milbanke', 'George Gordon Byron', 'Catherine Gordon', 'Captain John Byron', 'Catherine Gordon', 'Captain John Byron', 'Catherine Gordon', 'Captain John Byron', 'Catherine Gordon', 'Captain John Byron'],
    'Byron King-Noel': ['Augusta Ada King', 'William King-Noel'],
    'George Gordon Byron': ['Catherine Gordon', 'Captain John Byron'],
    'John Byron': ['Vice-Admiral John Byron', 'Sophia Trevannion']}

由於我們使用的result實際就是詞典中的value列表的引用,改動了result,就也改動了ada_family詞典,從而導致結果不正確(有很多重複項)。
修改為如下寫法即可:

def ancestors(genealogy, person):
    if person in genealogy:
        parents = genealogy[person]
        result = parents
        for parent in parents:
            result = result + ancestors2(genealogy, parent)
        return result
    return []

print ancestors2(ada_family, 'Judith Blunt-Lytton')
print ada_family


>>> ['Anne Isabella Blunt', 'Wilfrid Scawen Blunt', 'Augusta Ada King', 
    'William King-Noel', 'Anne Isabella Milbanke', 'George Gordon Byron', 
    'Catherine Gordon', 'Captain John Byron']
>>> {'Ralph King-Milbanke': ['Augusta Ada King', 'William King-Noel'], 
    'Ada King-Milbanke': ['Ralph King-Milbanke', 'Fanny Heriot'], 
    'Anne Isabella Blunt': ['Augusta Ada King', 'William King-Noel'], 
    'Augusta Ada King': ['Anne Isabella Milbanke', 'George Gordon Byron'], 
    'Judith Blunt-Lytton': ['Anne Isabella Blunt', 'Wilfrid Scawen Blunt'], 
    'Byron King-Noel': ['Augusta Ada King', 'William King-Noel'], 
    'George Gordon Byron': ['Catherine Gordon', 'Captain John Byron'], 
    'John Byron': ['Vice-Admiral John Byron', 'Sophia Trevannion']}

此時就不會修改原詞典內容,得到的也是正確結果。
當然,我們也可以簡單的使用以下方法實現,就避免了引用帶來的麻煩:

def ancestors(genealogy, person):
     if person in genealogy:
         parents = genealogy[person]
         return parents + ancestors(genealogy,parents[0]) + ancestors(genealogy,parents[1])
     return []

Example 10

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

預想應當是:

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

但結果卻為何要賦值無限次?
Python 做的事情是把 values 這個標籤所引用的列表物件的第二個元素指向 values 所引用的列表物件本身。執行完畢後,values 標籤還是指向原來那個物件,只不過那個物件的結構發生了變化,從之前的列表 [0, 1, 2] 變成了 [0, ?, 2],而這個 ? 則是指向那個物件本身的一個引用。
可以說 Python 沒有賦值,只有引用。上述操作相當於建立了一個引用自身的結構,所以導致了無限迴圈。
value[3]
要達到你所需要的效果,即得到 [0, [0, 1, 2], 2] 這個物件,你不能直接將 values[1] 指向 values 引用的物件本身,而是需要吧 [0, 1, 2] 這個物件「複製」一遍,得到一個新物件,再將 values[1] 指向這個複製後的物件:

values[1] = values[:]

當然這裡也僅僅是淺層複製,適用於單層結構,而深層複製則需要用到copy.deepcopy()


References

[1] python 引用和物件理解 —— ShaunChen
[2] 關於Python中的引用 —— 東去春來
[3] python基礎(5):深入理解 python 中的賦值、引用、拷貝、作用域 —— xrzs

希望能夠都對大家有所幫助~