Python進階 -- 變量、深拷貝和淺拷貝

Python中的變量

Python中變量并不是我們常說的一個“箱子”,而是在對象上貼的“標(biāo)簽”,因為是標(biāo)簽,所以多個變量可以指向同一個箱子。

可以看下面的例子:

x = ('a', 'b')
y = ('a', 'b')

print(x is y) # True
print(id(x[-1])) # 4507066072
print(id(y[-1])) # 4507066072

在Cpython中,id可以返回一個對象的存儲地址,我們可以看到,對于元組和字面常量這樣的不可變對象,python為了節(jié)省開銷,是將標(biāo)簽x,y貼在了同一個存儲空間上,而不是為x和y分別建立“箱子”,再把元素放進去。

那么對于可變元素呢?

x = ['a', 'b', [1, 2]]
y = ['a', 'b', [1, 2]]
print(x is y) # False
print(id(x[1])) # 4507066072
print(id(y[1])) # 4507066072
print(id(x[-1])) # 
print(id(y[-1])) # 

我們可以用http://www.pythontutor.com/visualize.html#mode=display 這個網(wǎng)站來可視化python的執(zhí)行,

對于上面的例子:


python中變量的存儲.png

可以看到我們在建立對象x和y時貼標(biāo)簽的過程。

淺拷貝

淺拷貝就是只復(fù)制對象及其包含的引用,但是對對象內(nèi)嵌套的對象不進行操作。

剛才看到用=將舊對象賦值給新變量的時候,其實只是將新變量指向了舊對象,而并沒有對對象進行拷貝。要對對象進行拷貝,最簡單的辦法是用該對象的constructor:

l1 = [3, [2, 1], [4, 5]]
l2 = list(l1) # 用constructor建立拷貝
print(l1 == l2) # True,值相等
print(l1 is l2) # False,地址不同

在用constructor對對象進行拷貝時,為了節(jié)省內(nèi)存,默認(rèn)進行淺拷貝。出了constructor以外,對可遍歷的對象用[:]也會創(chuàng)造拷貝,如l3 = l1[:]的效果等同于上面例子的第二條語句。

我們可以用下面的代碼為例,可視化的看看淺拷貝的過程:

x = ['a', 'b', [1, 2], (3, 4)]
y = list(x)

對應(yīng)的內(nèi)存模型:


python中的淺拷貝.png

如果拷貝的對象中都包含可變對象,那么淺拷貝可能會造成一些問題:對一個變量的修改會造成另一個指向相同對象的變量值變化。

深拷貝

copy模塊中的deepcopy()可以創(chuàng)造一個深拷貝。深拷貝不但會拷貝對象,對對象內(nèi)嵌套的對象也會進行拷貝,而非創(chuàng)建引用。

看下面的例子:

from copy import copy, deepcopy


class SomeClass(object):
    def __init__(self, inner_lst=[]):  # 用可變對象作為默認(rèn)參數(shù)非常不好,這里只是示例,實際代碼中不要這樣操作
        self.lst = inner_lst


if __name__ == '__main__':
    sc = SomeClass()
    sc_shallow_copy = copy(sc)  # 創(chuàng)建一個淺拷貝
    sc_deep_copy = deepcopy(sc)  # 創(chuàng)建一個深拷貝
    # 打印三個對象的地址
    print(id(sc), id(sc_shallow_copy), id(sc_deep_copy))  # 4404379720 4404380056 4405153976
    # 打印三個對象中嵌套對象的地址
    print(id(sc.lst), id(sc_shallow_copy.lst), id(sc_deep_copy.lst))  # 4405032392 4405032392 4404061192

可以看到,三個對象的內(nèi)存地址都是不同的,可見并非引用,而是各自建立了一份拷貝;但是對于對象之中嵌套的list對象,淺拷貝只拷貝了引用,而深拷貝則為其也創(chuàng)建了拷貝,這就是深淺拷貝的主要區(qū)別所在。

拷貝中的例外

在Cpython解釋器中,為了減少內(nèi)存的消耗,對于不可變類型的拷貝實際上會采用引用的形式,也就是在原先的內(nèi)存內(nèi)容上,貼上一個新的標(biāo)簽,而不會真的在一塊新內(nèi)存中建立一份拷貝??梢钥聪旅娴睦樱?/p>

if __name__ == '__main__':
    some_lst = [1, 2, []]
    some_lst_cpy = list(some_lst)  # 創(chuàng)建一個淺拷貝
    some_lst_cpy2 = some_lst[:]  # 創(chuàng)建一個淺拷貝
    print(some_lst is some_lst_cpy)  # False
    print(some_lst is some_lst_cpy2)  # False

    some_tuple = (1, 2, [])
    some_tuple_cpy = tuple(some_tuple)  # 試圖創(chuàng)建一個淺拷貝,但是實際上只會創(chuàng)建一個reference
    some_tuple_cpy2 = some_tuple[:]  # 試圖創(chuàng)建一個淺拷貝,但是實際上只會創(chuàng)建一個reference
    print(some_tuple is some_tuple_cpy)  # True
    print(some_tuple is some_tuple_cpy2)  # True

可以看到,對于可變類型list,解釋器真的創(chuàng)建了一份拷貝;但是對于不可變類型tuple,解釋器只是建立了一個引用(盡管這里的不可變類型并非絕對不可變的,對tuple中的list進行修改,仍然會改變這個tuple的值)。同樣的情況會發(fā)生在對字符串字面量,數(shù)字字面量(數(shù)字較小時),frozenset進行拷貝時。

這是Cpython解釋器的一個優(yōu)化,叫做內(nèi)化(interning),但是并非對所有的字符串字面量和數(shù)字字面量都是如此,到底對符合哪些標(biāo)準(zhǔn)的字符串和數(shù)字進行內(nèi)化的具體實施細(xì)節(jié)尚不清楚。

函數(shù)參數(shù)傳遞中的淺拷貝

在C++中,參數(shù)傳遞有幾種經(jīng)典的方式:pass by reference, pass by value, pass by address。但是在python中,所有參數(shù)傳遞只有一種方式:pass by sharing。其實也就是只有引用傳遞,所有傳遞入函數(shù)的形參都是實參的別名(引用)。

因此在傳遞參數(shù)時,需要特別注意將可變對象作為參數(shù)傳遞,因為函數(shù)中是可能改變這些變量的值的,因此在寫代碼之前我們要充分思考我們的意圖是否是要改變這些可變對象的值,否則就可能產(chǎn)生意料之外的后果。

class Names(object):
    """ 一個公司花名冊 """

    def __init__(self, inner_lst=None):
        if inner_lst is None:
            self.lst = []
        else:
            self.lst = inner_lst


if __name__ == '__main__':
    some_people = ["Xiao Zhang", "Xiao Wang", "Lao Li"]  # 公司老員工
    n = Names(some_people)
    n.lst.append("Lao Xiao")  # 新加入員工
    print(some_people)  # ['Xiao Zhang', 'Xiao Wang', 'Lao Li', 'Lao Xiao']

由于前面說的引用機制,Names類在進行對象初始化時,實際上會將self.lst作為一個引用,指向inner_lst,因此在對Names類對象的lst進行操作時,操作也會影響到inner_lst。這會造成一些問題:例如我們要區(qū)分公司老員工和今年新加入員工,在用上面這個花名冊操作之后,我們發(fā)現(xiàn)這個新加入員工同樣也被加入了老員工的列表,這和我們的預(yù)期是不符合的。

要解決上面這個問題很簡單,了解了引用和拷貝的區(qū)別之后,我們只需要進行一個拷貝,就可以防止發(fā)生這樣的問題:

class Names(object):
    """ 一個公司花名冊 """

    def __init__(self, inner_lst=None):
        if inner_lst is None:
            self.lst = []
        else:
            self.lst = list(inner_lst)  # 用constructor創(chuàng)建一個拷貝


if __name__ == '__main__':
    some_people = ["Xiao Zhang", "Xiao Wang", "Lao Li"]  # 公司老員工
    n = Names(some_people)
    n.lst.append("Lao Xiao")  # 新加入員工
    print(some_people)  # ['Xiao Zhang', 'Xiao Wang', 'Lao Li']

Python中對象的生存周期

Python中的對象創(chuàng)建

前面已經(jīng)講過Python中的變量是類似于“貼標(biāo)簽”的過程,那么是先創(chuàng)建了一個對象,再將后創(chuàng)建的標(biāo)簽貼在它身上呢,還是先創(chuàng)建了標(biāo)簽,再將標(biāo)簽貼在后創(chuàng)建的對象上呢?也就是說,變量名和對象的創(chuàng)建,孰先孰后?

看下面的例子:

class SomeClass(object):
    def __init__(self):
        print("Memory address of current object: ", id(self))


if __name__ == '__main__':
    x = SomeClass()  # Memory address of current object:  4559627936
    y = SomeClass() + 5  # Memory address of current object:  4559999048
    # 會raise一個error,因為沒有定義SomeClass類的"__add__"方法、

從這例子可以看到,是先創(chuàng)建了一個對象,后創(chuàng)建變量(也就是“標(biāo)簽”),再將標(biāo)簽貼在這個變量上的。

Python中的垃圾回收

程序自然不能無限制使用內(nèi)存,事實上,程序使用的內(nèi)存越少,后續(xù)操作能夠順利執(zhí)行的可能性就越大。這就是說當(dāng)程序不再需要某個對象時,需要有一種機制將這些不用的對象所占據(jù)的內(nèi)存進行釋放,這個機制就叫做垃圾回收。

CPython中最主要的垃圾回收機制就是引用計數(shù)(reference counting)。每個對象都有一個計數(shù)器,統(tǒng)計有多少個指向它的引用。當(dāng)這個引用數(shù)清零之后,CPython會喚起對象的__del__方法來刪除對象并釋放該對象占用的內(nèi)存。在CPython2.0中,垃圾回收機制會檢測循環(huán)引用(也就是a指向b,b指向c,c指向a的這類情況),將這組只有循環(huán)引用的對象也進行垃圾回收。

為了驗證,可以看下面例子:

import weakref


def send_message():
    print("Object destroyed!")


if __name__ == '__main__':
    x = {1, 2, 3}
    status = weakref.finalize(x, send_message)  # 類似于一個裝飾器,在對象被回收時運行send_message程序
    y = x
    del x  # 刪除引用x
    # 檢查對象{1, 2, 3}是否仍然存在
    print(status.alive)  # True
    del y
    print(status.alive)  # Object destroyed! False

weakref會創(chuàng)建一個弱引用,即不累加對象中引用計數(shù)器的引用。在開始時我們的對象{1, 2, 3}有兩個引用x,y指向它,在刪除了x之后,對象仍然存在;但是在y被刪除后,不再有指向該對象的引用,因此對象被回收。

從這個例子中我們還可以發(fā)現(xiàn),del刪除的是“標(biāo)簽”,也就是引用,而不是對象。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容