第八章《对象引用、可变性和垃圾回收》(下)
對《流暢的python》的讀書筆記以及個人理解
9-20
接著上一篇的內容,上一篇的鏈接:https://blog.csdn.net/scrence/article/details/100905929
8.4 函數的參數作為引用時
Python唯一支持的參數傳遞模式是共享傳參,這種機制類似于C++中的按引用傳參,但是也有一些區別。
共享傳參指函數的各個形式參數獲得實際參數中各個引用的副本,也就是說,函數內部的形參是實參的別名。這種方案的結果是,函數可能會修改作為參數傳入的對象,但是無法修改那些對象的標識(即不能把一個對象替換成另一個對象)。
在函數中,一般將定義函數時設置的參數稱為形式參數,在真正調用函數時給這個函數傳遞的參數稱為實際參數,python的參數傳遞機制規定,作為參數傳入的可變對象,函數在運算后,可能會修改原來的對象。
如何理解這句話?首先要注意,什么參數傳進函數可能會被改變?是可變對象作為參數傳進函數的時候會被改變。可變對象最常見的有列表對象,字典對象等等,如果是int,str,tuple這種不可變對象,那么傳進函數后是不會對原來的對象有影響的。其次,可能會修改原來的對象?那什么情況會修改?在函數內部代碼涉及操作參數的時候。下面通過代碼來說明:
這里定義了一個簡單的函數,它簡單地將傳遞進來的兩個參數相加,這是涉及了對參數的修改,現在來看看對于可變對象和不可變對象,這段代碼會產生什么樣的影響:
>>> x = 10 # 不可變對象 >>> y = 20 >>> f(x, y) >>> x 10 >>> y 20可以看到,經過函數的操作后,原來的x和y并沒有什么不同,這是正常的,因為它們是不可變對象。
對于可變對象,情況有些不同:
對于列表對象而言,函數f修改了原對象的內容。
8.4.1 不要使用可變類型作為參數的默認值
可選參數可以有默認值,這個是語言的特性。但是,應該盡量避免使用可變的對象作為參數的默認值。下面在代碼中說明這個問題。
class HauntedBus:"""備受幽靈乘客折磨的校車"""# 以上一個Bus類為基礎,在__init__()中修改參數passengers的默認值# 將None改為一個空列表對象,這樣就不用進行判斷了# 這種看似聰明的方法卻會帶來巨大的麻煩# 如果沒傳入參數,passengers會默認為空列表def __init__(self, passengers = []): # 這個賦值語句將self.passengers變為passengers的別名,# 如果沒有傳入參數,它又是空列表的別名self.passengers = passengers# 調用pick和drop方法時,修改的其實是默認列表,它是函數的一個屬性def pick(self, name):self.passengers.append(name)def drop(self, name):self.passengers.remove(name)接下來演示其詭異行為:
>>> bus1 = HauntedBus(['Alice', 'Bill']) >>> bus1.pick('Charlie') >>> bus1.drop('Alice') >>> bus1.passengers ['Bill', 'Charlie'] # 目前來看bus1是正常的>>> bus2 = HauntedBus() # 現在創建bus2,它是默認列表 >>> bus2.pick('Carrie') # bus2添加一個學生 >>> bus3 = HauntedBus() # 創建bus3,它也使用默認列表 >>> bus3.passengers # 訪問bus3的passengers,問題來了 ['Carrie']bus3明明使用的默認列表,卻憑空出現了一個元素,而這個元素,很顯然是在bus2中被添加進去的,這意味著,bus2和bu3內部的passengers又共享了,下面的代碼再次驗證了這一點:
>>> bus3.pick('Dave') # 向bus3添加一個學生 >>> bus2.passengers # 訪問bus2的列表 ['Carrie', 'Dave']>>> bus2.passengers is bus3.passengers # 上一篇文章說過,is 運算符會比較對象的地址 True既然bus2和bus3的列表是共享的,那么,bus1的呢?
>>> bus1.passengers ['Bill', 'Charlie']很明顯,bus1的列表跟其他對象并沒有共享。
問題在于,沒有指定初始乘客的HauntedBus實例會共享同一個乘客列表。
實例化HauntedBus時,如果傳入乘客,會預期運作。但是不為HauntedBus指定乘客時,奇怪的事情就發生了,這是因為self.passengers變成了passengers參數默認值的別名。出現這個問題的根源是,默認值在定義函數時計算(通常在加載模塊時),因此默認值變成了函數對象的屬性。因此,如果默認值是可變對象,而且修改了它的值,那么后續的函數調用都會受到影響。
可變默認值導致的這個問題說明了為什么通常使用None作為接收可變值的參數的默認值。使用None默認值不會變為函數函數內的一個屬性,也就不存在共享。
在Keras的源碼中有大量函數的參數都使用的None,而不是一個可變對象,防御任何可能出現異常的情況保證了框架代碼的正確執行。
9-22
8.4.2 防御可變參數
如果定義的函數接收可變對象,應該謹慎考慮調用方是否期望修改傳入的參數。
若函數對可變對象進行了修改操作,那么這種操作所帶來的影響要不要體現在函數外部?這需要編寫者與調用方達成共識。
重新定義一個類來演示這種問題:
class TwilightBus:"""讓乘客銷聲匿跡的校車"""def __init__(self, passenegers = None):if passenegers is None:self.passengers = [] # 當passengers為空時,創建一個新的列表else:# 這個賦值語句將self.passengers變為傳入__init__()的參數passengers# 的別名。self.passengers = passenegers# 在self.passenegrs上做修改操作實際上會修改傳入的構造方法的實參def pick(self, name):self.passengers.append(name)def drop(self, name):self.passengers.remove(name)TwilightBus類的實例與客戶共享乘客列表,這會產生意外的結果:
>>> basketball_team = ['Sue', 'Tina', 'Maya', 'Diana', 'Pat'] >>> bus = TwilightBus(basketball_team) >>> bus.drop('Tina') >>> bus.drop('Pat') >>> basketball_team ['Sue', 'Maya', 'Diana']在這個例子中,basketball_team是一個獨立的列表對象,作為參數傳入TwilightBus的構造方法之后,其類對象做的修改卻影響到了basetball_team的內容,很明顯,這并不是我們想要的內容,因為basketball_team與類實例中的passengers共享的是同一個列表對象,要想解決這個問題,就需要修改TwilightBus類的__init__(self, passenegers = None):
def __init__(self, passenegers = None):if passenegers is None:self.passengers = []else:# 這里用list()復制,而不是簡單地起了別名self.passengers = list(passenegers)在最后一行代碼部分,使用了list()復制了passengers參數,使得類實例以參數為模板,創建出了它的副本,這兩個列表之間是不一樣的對象(使用id()可以知道),他們內容相同,但是地址不同。這樣的解決方法可以使類對象不至于修改可變的參數對象,除非這個方法確實需要修改傳入的參數對象,否則在類中直接把參數賦值給實例變量之前一定要謹慎,因為這樣會為參數對象創建別名。如果不確定方法是否真的需要修改原參數,那就直接創建副本,這樣會少一些麻煩。
插個題外話,這可能是原作者不愿使文章篇幅太長而略過了這個問題,我在這里做個補充。
其實使用list(passengers)這種方式還是存在一些問題,因為list(passengers)這種代碼實際上只是淺復制,這樣做要想真正地不修改原參數還是不夠嚴謹。我們之所以要防御可變參數,根源在于可變參數與類內部自己的屬性對象會存在共享,而我們的解決方法就是讓他們不共享,真正地創建出一個完全獨立的對象。然而,list()這種方法并不是完全地創建出獨立對象,因為它是淺復制,在復制對象的時候,對于passengers列表中的可變對象,復制時仍然會把它設為共享的,這樣是為了節省內存。假如說,現在原basketball_team對象是這樣的:['Sue', ['Tina', 'Maya', 'Diana', 'Pat']] ,Sue是籃球隊的教練,指揮一個隊伍,這個隊伍用列表包裝起來,那么這個隊伍實際上還是可變參數,在TwilightBus的構造方法內部簡單地使用list()復制basketball_team時,對于籃球隊,self.passengers與passengers仍然是共享的,這是淺復制所帶來的結果,若類方法針對籃球隊伍做修改,比如說,上了公交車的籃球隊中,成員Tina因公交車位不足而改坐別的車,那么類方法就需要對籃球隊中的隊伍做修改,將列表['Sue', ['Tina', 'Maya', 'Diana', 'Pat']]修改為['Sue', ['Maya', 'Diana', 'Pat']],那么由于self.passengers與basketball_team共享了 ['Tina', 'Maya', 'Diana', 'Pat']這個列表,在類方法中針對隊員隊伍做的修改會影響到原來basketball_team中的隊員隊伍:
運行結果為['Sue', ['Maya', 'Diana', 'Pat']]。
可以看到,因為公交車座位不足,隊員Tina去了別的公交車,而方法直接將隊員Tina開除出了籃球隊。當然,drop的代碼是我專門針對basketball_team這個列表編寫的,目的是為了演示。那么為了不使Tina因為車位不足就被開除出隊,我們就要解決問題,問題的根源仍然是對象共享的問題,為了完完全全使basketball_team與self.passengers隔絕開來,就需要使用內置模塊copy中的deepcopy()方法來完成這個任務(具體內容參見于上一篇:
第八章《對象引用、可變性和垃圾回收》(上)中的8.3 默認做淺復制):
這樣一來,上面的代碼就不會將Tina開除出隊了。
8.5 del 和垃圾回收
del語句刪除名稱,而不是對象。僅當刪除的變量保留的是對象的最后一個引用,或者無法得到對象時,,del命令可能導致對象被當做垃圾回收。另外,重新綁定也可能會導致對象的引用數量歸零,導致對象被銷毀。
在Cpython中,垃圾回收使用的主要算法是引用計數。實際上,每個對象都會統計有多少個引用指向自己,當引用計數歸零時,對象會被立即銷毀:Cpython會在對象上調用__del__方法,然后釋放分配給對象的內存。python的其他實現有更復雜的垃圾回收程序,且不依賴與引用計數,這意味著,對象引用計數歸零時可能不會立即調用__del__方法。
為什么{1,2,3}對象被銷毀了?畢竟,代碼將s1引用傳遞給finalize函數了,而為了監控對象和調用回調,必須要有引用,既然有引用,為什么set對象仍然被銷毀?這是因為finalize持有的是{1,2,3}的弱引用。
8.6弱引用
因為有引用的存在,對象才得以在內存中存在。當對象的引用清零后,垃圾回收程序會將對象銷毀。
有一類特殊的引用,弱引用,它指向對象時,并不會增加對象的引用計數,因此,弱引用并不會影響垃圾回收程序回收對象,它只是在對象引用清零時,在回收程序即將回收這個對象之前,仍然保留這個對象的引用以做一些用途,但當回收程序開始回收時,弱引用不會影響程序的工作。
弱引用在緩存中很有用,因為我們不想僅僅因為緩存對象被引用著而始終保留緩存對象。
下面的例子是一個控制臺會話,python控制臺會自動把_變量綁定到結果不為None的表達式結果上。
弱引用是可調用的對象,返回的是被引用的對象;如果所指對象不存在了,返回None
>>> import weakref >>> a_set = {0, 1} >>> wref = weakref.ref(a_set) # 創建弱引用對象wref >>> wref <weakref at 0x000002359F13C4F8; to 'set' at 0x000002359EE75E48> >>> wref()# 調用wref()返回的是被引用的對象,{0,1}。因為這是控制臺會話,所以{0,1}會綁定給 _ 變量 {0, 1} >>> a_set = {2,3,4} # a_set不再指代{0,1}集合,因此集合的引用數量減少了。但是 _ 變量仍然指代它。 >>> wref() # 調用wref()仍然返回{0,1} {0, 1} >>> wref() is None # 計算這個表達式的時候,{0,1}存在,因為_變量指向它,但是隨后_綁定到表達式結果False。現在{0,1}沒有強引用了。 False >>> wref() is None 因為{0,1}對象已被清理,所以wref()返回None Trueweakref.ref類是偏底層的接口,多數程序最好使用weakref集合和finalize。也就是說,更推薦使用WeafKeyDictionary、WeakValueDictionary、WeakSet和finalize。
8.6.1 WeakValueDictionary簡介
WeakValueDictionary類實現的是一種可變的映射,里面的值是對象的弱引用。被引用的對象在程序中的其他地方被回收后,對應的鍵會自動從WeakValueDictionary中刪除。
class Cheese:def __init__(self, kind):self.kind = kind >>> import weakref >>> stock = weakref.WeakValueDictionary() # stock是WeakValueDictionary類實例 >>> catalog = [Cheese('Red Leicester'), Cheese('Tilsit'), Cheese('Brie'), Cheese('Parmesan')] >>> for cheese in catalog: ... stock[cheese.kind] = cheese # stock將kind映射到catalog中Cheese實例的弱引用上。>>> sorted(stock.keys()) ['Brie', 'Parmesan', 'Red Leicester', 'Tilsit'] # stock是完整的 >>> del catalog >>> sorted(stock.keys()) ['Parmesan'] # 刪除catalog之后,stock的大部分元素不見了,只剩最后一個元素Parmesan,為什么不是全部呢? >>> del cheese # 刪除全局變量cheese >>> sorted(stock.keys()) []上面的代碼之所以剩下Parmesan是因為臨時變量cheese引用了這個對象,它使得該變量存在的時間比預期長。通常,這對局部變量來講不是問題,因為它們會在函數范湖時被銷毀,但在上面的代碼中,for循環的cheese是全局變量,除非顯式刪除,否則不會消失。
8.6.2 弱引用的局限
不是每個python對象都可以作為弱引用的目標。基本的list和dict實例都不能作為弱引用的目標,但它們的子類卻可以。
set和用戶自定義類型都可以作為弱引用的目標,而對于int和tuple,不僅它們本身不行,它們的子類也不行。
這些局限基本上是CPython的實現細節,其他的python解釋器可能不一樣,這些局限是內部優化的結果。
8.7 python對不可變類型施加的把戲
對于元組來講,t[:]和tuple(t)都返回的是同一個元組的引用(對于list來講則是淺復制,創建一個副本):
>>> t1 = (1,2,3) >>> t2 = tuple(t1) >>> t2 is t1 # t2 與t1 綁定在同一個對象 True >>> t3 = t1[:] >>> t3 is t1 # t3也是 Truestr、bytes和frozenset實例也有這種行為。
>>> s1 = 'ABC' >>> s2 = 'ABC' >>> s1 is s2 True很明顯,s1和s2是兩個字符串對象,但使用is比較時,卻仍然返回True,這其實是一種優化措施,稱為駐留,s1和s2是共享的字符串字面量。Cpython還會在比較小的整數上使用這個優化措施,防止重復創建“熱門數字”。但是,千萬不要依賴于字符串或者整數的駐留,比較字符串或者整數是否相等時,要使用==運算符而不是is。
8.8 小結
每個Python對象都有標識、類型和值。只有對象的值會不時變化。
如果兩個變量指代的不可變對象具有相同的值(a == b 為 True),實際上跟他們指代的是副本還是同一個對象的別名基本沒什么關系,因為不可變對象的值不會變,但有一個例外,這里說的例外是不可變的集合,如元組和frozenset:如果不可變集合保存的是可變元素的引用,那么可變元素的值發生變化后,不可變集合也會隨之改變,這是不可變集合的相對不變性。但實際上,這種情況并不常見。不可變集合不變的是所含對象的標識。
變量保存的是引用,這一點對編程有很多實際的影響:
1、簡單的賦值不創建副本。
2、對+= 或者 *= 所做的增量賦值來說,如果左邊的變量綁定的是不可變對象,那么增量賦值會創建新的對象,如果是可變對象,會就地修改。
3、為現有的變量賦予新值,不會修改之前綁定的變量。這叫重新綁定:現在變量綁定了其他對象。如果變量是之前那個對象最后的引用,那么對象會被當做垃圾回收
4、函數的參數以別名的形式傳遞,這意味著,函數可能會修改通過參數傳入的可變對象。這一行為無法避免,除非在本地創建副本,或者使用不可變對象(例如:傳入元組,而不是列表)。
5、使用可變類型作為函數參數的默認值有危險,因為如果就地修改了參數,默認值也就變了,這會影響以后使用默認值的調用
在CPython中,對象的引用數量歸零后,對象會被立即銷毀。如果除了兩個循環引用之外沒有其他引用,兩個對象都會被銷毀。在某些情況下,可能需要保存在對象的引用,但不保留對象本身。例如,有一個類想要記錄所有實例。這個需求可以使用弱引用實現,這是一種低層機制,是weakref模塊中WeakValueDictionary、WeakKeyDictionary和WeakSet等有用的集合類,以及finalize函數的底層支持。
對于weakref類,可以參考官方文檔:https://docs.python.org/3/libary/weakref.html
總結
以上是生活随笔為你收集整理的第八章《对象引用、可变性和垃圾回收》(下)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Eclipse安装STS插件
- 下一篇: 架构风格与基于网络的软件架构设计