坑出沒 Python 中引用型型別淺拷貝

2021-09-22 22:29:28 字數 2926 閱讀 6355

python 中的物件有兩種型別,一種是值型別,一種是引用型別。值型別的代表有 int,而今天的主角引用型別有 list、set、dict 等。

引用型別指的是:

a = [1, 2, 3]
在物件 a 中儲存的是乙個指標,這個指標指向陣列 [1, 2, 3] 的底層資料,類似與 c++ 中的 vector。

那麼什麼叫淺拷貝呢?以下**

shallow_cpy = a
shallow_cpy 就是對 a 這個引用型別進行一次淺拷貝(也叫做別名),雖然 a 與 shallow_cpy 是兩個不同的變數,但是他們實際都是指向同一片記憶體空間。這就像乙個人的中文名和英文名,雖然聽著是不同的東西,但是其實都是對應乙個人一樣。任何對 a 的修改,最終也都會體現在 shallow_cpy 上(反之亦然)。

那麼這會導致什麼問題呢?不妨看乙個實際場景:

我有乙個簡易爬蟲,它的輸入是乙個 url set,因為這個爬蟲執行時間很長,我們希望程式中可以加入乙個功能,支援這個程式重新啟動以後可以跳過之前處理過的 url。因為在單機執行,我決定使用 python 自帶的 pickle 模組 + 檔案系統來做持久化儲存,於是有下面的非常 ***** **:

cache = {}

def load():

"""load pickle from filesystem"""

pass

def dump():

"""dump cache dict as a pickle file"""

pass

def set_url_set_if_not_exist(main_page, url_set):

if main_page in cache:

return

cache[main_page] = url_set

def remove_url(main_page, url):

assert main_page in cache

cache[main_page].remove(url)

def craw(main_page, url_set):

set_url_set_if_not_exist(main_page, url_set)

for url in url set:

# blabla

# done with url

print url

remove_url(main_page, url)

craw('', ['url1', 'url2', 'url3', 'url4'])

現在回首看這段**,著實讓我汗顏。這個功能的實現簡直不知所謂。各位看官,不妨先問一下自己這個**的問題在那?print 會輸出那些 url ?

答案揭曉:

url1

url3

這是怎麼回事, url_set 明明有4個元素,怎麼 for 迴圈只輸出了 2 個呢?這口鍋,只能讓咱們的淺拷貝來背了(或者讓我這個菜雞自己背也可以)。

問題的始發地在 set_url_set_if_not_exist(main_page, url_set) 上,這個函式中,我們將 cache[main_page] 賦值為 url_set,但是這個僅僅是對 url_set 的乙個淺拷貝,此時 cache[main_page] 和 url_set 都指向同乙個陣列(['url1', 'url2', 'url3', 'url4'])。而

def craw(main_page, url_set):

set_url_set_if_not_exist(main_page, url_set)

for url in url set:

# blabla

# done with url

print url

# 其實是在 for 遍歷 url_set 的期間對集合進行了修改

remove_url(main_page, url)

for 迴圈中,對 remove_url(main_page, url) 的呼叫,其實就是在對被迴圈物件 url_set 的修改。這個其實就違背了乙個使用迭代器的基本原則:使用迭代器期間要保證迭代器的有效性。

這裡有必要先解釋一下,在 python 中,for 迴圈的機制如下面**所示:

for i in iterable_obj:

pass

# 等價於

it = iter(iterable_obj)

while true:

try:

i = it.next()

# inside loop

except stopiteration:

break

對於乙個 iterable object(iterable 指的是一種介面契約,所有符合這種契約的物件都可以成為 iterable object),python 會先用 iter() 方法獲取其迭代器,一直呼叫迭代器的 next 方法直到丟擲 stopiteration 的異常,則結束迴圈。

而迭代器思想,指的就是通過提供迭代器去遍歷乙個資料集合的思想。要想保證 for 迴圈的正常執行,這個迭代器在 for 迴圈期間必須是有效的。

如果我們修改了資料集合,那麼之前的任何迭代器,就很可能會失效了。這時候,it.next() 的行為是未定義的。不同的資料集合實現會導致這種情況下不同的輸出(set 或者 tuple 的結果大家可以自己去嘗試以下)。

可以看到,因為我們在 cache[main_page] 中儲存的乙份淺拷貝,所以對這個淺拷貝的增刪修改,其實就是在修改 url_set,這就導致了 for 迴圈的未定義行為了。

其實不光是迴圈,如果對乙份資料,存在多個淺拷貝(別名),那麼使用其中任意乙個別名對資料進行修改,其他的別名都會受到影響。這種行為,有時候可能是我們需要的(比如一些 input&output 引數),有些又往往會讓我們大跌眼睛,完全沒有頭緒。

python值型別與引用型別

物件本身不允許修改,數值的修改實際上是讓變數指向了乙個新的物件 包含 字串 元組 數值,本身不允許被修改 修改值型別的值,只是讓它指向乙個新的記憶體位址,並不會改變變數a的值 物件本身可以修改,包含 列表 字典,本身允許修改 修改引用型別的值,因為listb的位址和lista的一致,所以也會被修改 ...

python中的a 1 型別

for value in rang 10 涉及的數字倒序輸出 for value in rang 10 1 涉及的數字倒序輸出 一 反轉 二 詳解 這個是python的slice notation的特殊用法。a 0,1,2,3,4,5,6,7,8,9 b a i j 表示複製a i 到a j 1 以...

值型別與引用型別(中

本文將介紹以下內容 1.引言 上回 第八回 品味型別 值型別與引用型別 上 記憶體有理 的發布,受到大家的不少關注,我們從記憶體的角度了解了值型別和引用型別的所以然,留下的任務當然是如何應用型別的不同特點在系統設計 效能優化等方面發揮其作用。因此,本回是對上回有力的補充,同時應朋友的希望,我們盡力從...