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