第八章《对象引用、可变性和垃圾回收》(上)
對(duì)《流暢的python》的讀書筆記以及個(gè)人理解
9-16
8.1 變量不是盒子
python的變量類似于Java中的引用式變量,最好將它們理解為附加在對(duì)象上的標(biāo)注。變量并不是一個(gè)盒子,盒子中裝著對(duì)象內(nèi)容,而應(yīng)該是一個(gè)便利貼,貼在對(duì)象上的標(biāo)注,變量本身不是容器。
賦值的時(shí)候,應(yīng)該說“將變量分配給對(duì)象”,而不是“將對(duì)象分配給變量”,變量是虛無縹緲的東西,而對(duì)象才是真真正正存在于內(nèi)存中的,有實(shí)際意義的數(shù)據(jù)。
對(duì)象在賦值之前就已經(jīng)被創(chuàng)建了:
>>> class Gizmo: ... def __init__(self): ... print('Gizmo id: %d' % id(self)) >>> x = Gizmo() Gizmo id: 2238839532232>>> y = Gizmo() *10Gizmo id: 2238839532512 Traceback (most recent call last):File "<stdin>", line 1, in <module> TypeError: unsupported operand type(s) for *: 'Gizmo' and 'int'>>> dir() ['Gizmo', '__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'x'] >>>從上面的代碼可以清楚看出對(duì)象在賦值之前就已經(jīng)創(chuàng)建好了,對(duì)于Gizmo類的構(gòu)造方法,其中會(huì)打印出當(dāng)前創(chuàng)建對(duì)象在內(nèi)存中的id號(hào),因此在創(chuàng)建對(duì)象并賦值給x的時(shí)候,會(huì)直接打印出對(duì)象的id值。而在賦值給y這個(gè)變量的時(shí)候,可以看到依然打印出了對(duì)象的id,但隨后又報(bào)錯(cuò)了,這是因?yàn)?#xff0c;Gizmo()*10這行代碼,Gizmo()依然會(huì)創(chuàng)建對(duì)象,但Gizmo對(duì)象不可能與int對(duì)象做乘法,因此報(bào)錯(cuò),賦值給y也就無從說起,最后一行代碼dir()打印出了當(dāng)前目錄存在的變量,可以看到?jīng)]有y,因?yàn)樵趛 = Gizmo()*10中,操作的熟悉是先創(chuàng)建Gizmo對(duì)象,再執(zhí)行乘法,最后賦值,而在執(zhí)行乘法的時(shí)候引發(fā)錯(cuò)誤而終止操作,因此y這個(gè)變量并沒有被創(chuàng)建出來。
為了理解Python的賦值語句,讀代碼的時(shí)候應(yīng)該從右邊讀起,對(duì)象在右邊創(chuàng)建或者獲取,在此之后左邊的變量才會(huì)綁定到對(duì)象上,這像是為對(duì)象貼上了標(biāo)簽,但毫無疑問的是,由于變量只是對(duì)象的標(biāo)簽,因此一個(gè)對(duì)象可以有多個(gè)標(biāo)簽,也就是一個(gè)對(duì)象可以有多個(gè)變量標(biāo)注,這些變量便是別名。
9-17
8.2 標(biāo)識(shí)、相等性和別名
Lewis Carroll 是Charles Lutwidge Dodgson教授的筆名,Carroll指的就是Dodgson本人
>>> charles = {'name':'Charles L. Dodgson', 'born':1832} >>> lewis = charles >>> lewis is charles True >>> id(lewis) 1574035868624 >>> id(charles) 1574035868624 >>> lewis['balance'] = 950 >>> charles {'name': 'Charles L. Dodgson', 'born': 1832, 'balance': 950} >>>首先創(chuàng)建一個(gè)字典對(duì)象,將變量charles綁定到這個(gè)對(duì)象上,接著lewis = charles這行代碼表明lewis已經(jīng)成為字典對(duì)象的另一個(gè)別名,現(xiàn)在charles和lewis都是指這個(gè)對(duì)象,在內(nèi)存中,這兩個(gè)變量共享一個(gè)對(duì)象,使用id()查看其對(duì)象的id號(hào)可以知道。而lewis[‘balance’]=950則在字典中添加了一個(gè)新的鍵值對(duì),這使得用charles查看時(shí)其內(nèi)容也發(fā)生了變化。
接著上面的代碼:
現(xiàn)在,創(chuàng)建一個(gè)新的字典對(duì)象,用alex標(biāo)識(shí),對(duì)象的內(nèi)容與charles的相同,但charles和alex所指定的對(duì)象在內(nèi)存中是兩個(gè)截然不同的對(duì)象,他們的內(nèi)容相同,但是在內(nèi)存中的地址是不一樣的。
使用==運(yùn)算符可以看到true,而使用is運(yùn)算符則是false。可以看到, ==運(yùn)算符比較的是兩個(gè)對(duì)象的值,而is運(yùn)算符比較的則是兩個(gè)對(duì)象的標(biāo)識(shí)。
關(guān)于這個(gè)標(biāo)識(shí),可以使用id()方法來查看:
id()方法可以查看對(duì)象在內(nèi)存中的標(biāo)識(shí),可以理解為對(duì)象在內(nèi)存中的地址,在Cpython中,id()方法就是返回對(duì)象地址的整數(shù)表示,而is比較的,正是這個(gè)id()方法返回的標(biāo)識(shí)。
8.2.1 在==和is之間選擇
==運(yùn)算符比較兩個(gè)對(duì)象的值(對(duì)象中的數(shù)據(jù)),而is比較對(duì)象的標(biāo)識(shí)。這有點(diǎn)像C++對(duì)象,兩個(gè)C++對(duì)象做比較,重載運(yùn)算符= =方法一般直接比較對(duì)象中的值,而使用兩個(gè)分別指向這兩個(gè)對(duì)象的指針,指針之間比較的則是地址值。
在變量和單例值之間的比較時(shí),應(yīng)該使用is。最常見的is檢查莫過于判斷變量綁定的值是不是None。
否定的寫法:
x is not Noneis 運(yùn)算符不能重載,它的速度要比== 運(yùn)算符快,而 ==運(yùn)算符是由類中的__eq__()方法實(shí)現(xiàn),也就是說,a == b這種代碼,實(shí)際上是:
a.__eq__(b)所有類的eq()方法都繼承于object類,這個(gè)方法可以被覆蓋。
需要注意的是,基本類型(int, float, str…)的變量直接與底層的數(shù)據(jù)結(jié)構(gòu)掛鉤,只要兩個(gè)變量標(biāo)識(shí)的數(shù)據(jù)值相同,那么他們的id值也是相同的,即使用is會(huì)返回True:
8.2.2 元組的相對(duì)不可變性
都知道元組不可修改,但這并不完全正確。
元組中的元素保留的是對(duì)象的引用,也就是說(1,2,[1,2,3])這個(gè)元組中,元素1,元素2,元素[1,2,3]其實(shí)都只是引用,并不是真正的數(shù)據(jù),這一點(diǎn)造成了元組的相對(duì)不可變性,元組的不可變性其實(shí)指的是元組數(shù)據(jù)結(jié)構(gòu)中保留的引用不會(huì)變,但是引用真正指向的對(duì)象變化,跟元組本身沒有關(guān)系:
可以看到t1跟t2做比較,結(jié)果是True,因?yàn)闄z查了元組所有元素指向?qū)ο蟮膬?nèi)容,得到True。
>>> t1[-1] = 3 Traceback (most recent call last):File "<stdin>", line 1, in <module> TypeError: 'tuple' object does not support item assignment >>>現(xiàn)在嘗試對(duì)t[-1]做修改,結(jié)果自然是引發(fā)了異常,因?yàn)樵M需要維護(hù)它保留的元素的不變性,也就是說,t[-1]實(shí)際上是[1,2,3]這個(gè)list對(duì)象的引用,并不是list對(duì)象本身,我姑且用t_three這個(gè)引用來指代[1,2,3]這個(gè)對(duì)象,那么元組t1就是(1,2,t_three),元組維護(hù)的元素不變性,只是維持t_three這個(gè)引用\變量不被其他的變量改變,比如變成了t_four,這個(gè)是元組不允許的,但是元組t1對(duì)于[1,2,3]這個(gè)List對(duì)象,則沒有權(quán)限控制它不做改變,比如說:
>>> t1[-1].append(4) >>> t1 (1, 2, [1, 2, 3, 4])現(xiàn)在可以看到t1的內(nèi)容其實(shí)是被修改的了,但是這僅僅是對(duì)列表元素的修改,對(duì)于元組本身,它維護(hù)的內(nèi)容依然是各個(gè)對(duì)象的引用(1,2,t_three)。
默認(rèn)做淺復(fù)制
復(fù)制列表(或者多數(shù)內(nèi)置的可變集合)最簡單的方式是使用內(nèi)置的類型構(gòu)造方法。例如:
>>> l1 = [3,[55,44],(7,8,9)] >>> l2 = list(l1) >>> l2 [3, [55, 44], (7, 8, 9)] >>> l2 == l1 True >>> l2 is l1 Falselist(l1)這一句是對(duì)列表做的淺復(fù)制,在內(nèi)存中重新創(chuàng)建一個(gè)對(duì)象,新的對(duì)象綁定了變量l2,兩個(gè)對(duì)象的內(nèi)容相同,地址不同,因此使用 == 會(huì)返回True,使用is返回False。
但是,這種淺復(fù)制存在一些問題,對(duì)于列表元素,簡單類型(如int,float,str)在做淺復(fù)制時(shí)會(huì)重新在內(nèi)存中創(chuàng)建出另一個(gè)簡單類型的對(duì)象,但是對(duì)于list,set,tuple等內(nèi)置復(fù)雜類型或者其他自定義類來講,在執(zhí)行l(wèi)ist(l2)這種淺復(fù)制時(shí),并不會(huì)也像簡單類型那樣創(chuàng)建出新的對(duì)象再進(jìn)行綁定。換句話說,在上面的代碼里,l1和l2中的list和tuple元素使用的是同一個(gè)元素,而int元素則是不同的元素。
在l1中,列表元素[55,44]屬于可變型元素,這樣做淺復(fù)制,可能會(huì)導(dǎo)致一些意想不到的問題,下面對(duì)l1和l2做操作:
初始的時(shí)候,l1和l2內(nèi)容完全相同,這沒什么問題,但是l1.append(100)開始對(duì)l1添加一個(gè)尾部元素,而在l1[1].remove(55)中又將l1中的列表元素移除了55,前面說過,復(fù)雜類型的元素在做這種淺復(fù)制時(shí)并不會(huì)重新創(chuàng)建對(duì)象,而是采取共享的措施,這就出現(xiàn)了問題,在對(duì)基本元素操作時(shí),由于不是共享,因此對(duì)一個(gè)變量的操作不會(huì)影響到另一個(gè)變量所代表的對(duì)象,但是對(duì)于復(fù)雜類型,它們采取了共享措施,對(duì)一個(gè)對(duì)象復(fù)雜類型元素的操作會(huì)影響到另一個(gè)對(duì)象中的內(nèi)容,l1[1].remove(55)就是這樣,內(nèi)部的列表[55,44]由兩個(gè)對(duì)象共享,這樣一來,無論哪個(gè)對(duì)象對(duì)這個(gè)列表做操作,都會(huì)影響到另一個(gè)對(duì)象的內(nèi)容。
那么,其中的元組元素又是什么情況?
l1[2]是一個(gè)元組,它是不可變的對(duì)象,同時(shí)又是復(fù)雜類型,來看看對(duì)它的操作結(jié)果如何:
首先要清楚l1[2] += (10, 11)這個(gè)操作到底做了什么。
這個(gè)操作是合法的,重新創(chuàng)建了一個(gè)元組,這個(gè)元組內(nèi)容是(7,8,9,10,11),然后綁定到l1上,原來l1綁定的元組被丟棄掉,那么現(xiàn)在l1和l2的元組元素可就不是共享的了。在這之前,它們也是共享的,但由于元組并不能修改,我們不能寫出l1[2][1] = 5這種類型的代碼來,因?yàn)樵M維持的相對(duì)不變性不允許這種操作,而在使用+=這種操作之后,可以看到l1和l2的內(nèi)容并沒有相互混淆,這就是不可變元素帶來的好處了,雖然他們在內(nèi)部仍然是共享的,但是當(dāng)試圖對(duì)這些不可變元素做修改的時(shí)候,python就會(huì)強(qiáng)制讓它們不共享,重新創(chuàng)建出對(duì)象出來,因此不會(huì)互相影響,所以,對(duì)于不可變對(duì)象來說,淺復(fù)制是沒有問題的,而且還能節(jié)省內(nèi)存資源,但是對(duì)于可變對(duì)象來說,淺復(fù)制就不是那么安全了,要想做到可變對(duì)象的安全復(fù)制(復(fù)制后對(duì)于一個(gè)對(duì)象的操作不會(huì)影響到另一個(gè)對(duì)象的內(nèi)容),就要使用深層復(fù)制。
為任意對(duì)象做深層復(fù)制和淺復(fù)制
淺復(fù)制機(jī)制沒什么問題,但有時(shí)需要用到深復(fù)制(即副本不共享內(nèi)部對(duì)象的引用)。內(nèi)置模塊copy提供的copy()和deepcopy()可以實(shí)現(xiàn)為任意對(duì)象的淺復(fù)制和深復(fù)制。
9-20
定義一個(gè)簡單類來演示copy和deepcopy的用法:
class Bus:def __init__(self, passengers = None):if passengers is None:self.passengers = []else:self.passengers = list(passengers)def pick(self, name):self.passengers.append(name)def drop(self, name):self.passengers.remove(name)Bus類對(duì)象創(chuàng)建時(shí),會(huì)在內(nèi)部創(chuàng)建一個(gè)list,list屬于復(fù)雜對(duì)象,而且是可變對(duì)象,在做淺復(fù)制時(shí)并不安全。
>>> bus1 = Bus(['one', 'two', 'three']) #創(chuàng)建對(duì)象bus1 >>> bus2 = copy.copy(bus1) #淺復(fù)制bus1 >>> bus3 = copy.deepcopy(bus1) # 深復(fù)制bus1 >>> id(bus1), id(bus2), id(bus3) # 查看id,這是三個(gè)不同的對(duì)象 (2111764720832, 2111761750392, 2111762678112)>>> bus1.drop('three') #在bus1中移除元素three >>> bus1.passengers #可以看到bus1的passengers少了three ['one', 'two'] >>> bus2.passengers # 但是對(duì)bus1淺復(fù)制的bus2的passengers也受到了影響 ['one', 'two'] >>> bus3.passengers # 而深復(fù)制的bus3不會(huì)受到影響 ['one', 'two', 'three']>>> id(bus1.passengers), id(bus2.passengers), id(bus3.passengers) >>> # 分別查看三個(gè)對(duì)象的passengers,可以看到bus1和bus2共享同一個(gè)passengers (2111761482312, 2111761482312, 2111760949832)有時(shí)候深層復(fù)制可能復(fù)制的太深,一些需要共享的對(duì)象也被復(fù)制過來,這可以通過實(shí)現(xiàn)特殊方法__copy__()和__deepcopy()__來控制copy和deepcopy的行為,鏈接:https://docs.python.org/3/library/copy.html中有更多詳細(xì)信息。
先到這里,第八章內(nèi)容有些多
總結(jié)
以上是生活随笔為你收集整理的第八章《对象引用、可变性和垃圾回收》(上)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 普通用户的sudo权限,禁止root用户
- 下一篇: Eclipse安装STS插件