日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

第八章《对象引用、可变性和垃圾回收》(下)

發布時間:2025/3/15 编程问答 17 豆豆
生活随笔 收集整理的這篇文章主要介紹了 第八章《对象引用、可变性和垃圾回收》(下) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

對《流暢的python》的讀書筆記以及個人理解

9-20

接著上一篇的內容,上一篇的鏈接:https://blog.csdn.net/scrence/article/details/100905929

8.4 函數的參數作為引用時

Python唯一支持的參數傳遞模式是共享傳參,這種機制類似于C++中的按引用傳參,但是也有一些區別。

共享傳參指函數的各個形式參數獲得實際參數中各個引用的副本,也就是說,函數內部的形參是實參的別名。這種方案的結果是,函數可能會修改作為參數傳入的對象,但是無法修改那些對象的標識(即不能把一個對象替換成另一個對象)。

在函數中,一般將定義函數時設置的參數稱為形式參數,在真正調用函數時給這個函數傳遞的參數稱為實際參數,python的參數傳遞機制規定,作為參數傳入的可變對象,函數在運算后,可能會修改原來的對象。
如何理解這句話?首先要注意,什么參數傳進函數可能會被改變?是可變對象作為參數傳進函數的時候會被改變。可變對象最常見的有列表對象,字典對象等等,如果是int,str,tuple這種不可變對象,那么傳進函數后是不會對原來的對象有影響的。其次,可能會修改原來的對象?那什么情況會修改?在函數內部代碼涉及操作參數的時候。下面通過代碼來說明:

>>> def f(a, b): ... a += b

這里定義了一個簡單的函數,它簡單地將傳遞進來的兩個參數相加,這是涉及了對參數的修改,現在來看看對于可變對象和不可變對象,這段代碼會產生什么樣的影響:

>>> x = 10 # 不可變對象 >>> y = 20 >>> f(x, y) >>> x 10 >>> y 20

可以看到,經過函數的操作后,原來的x和y并沒有什么不同,這是正常的,因為它們是不可變對象。
對于可變對象,情況有些不同:

>>> v = [1,2] >>> w = [3,4] >>> f(v,w) >>> v [1, 2, 3, 4]

對于列表對象而言,函數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中的隊員隊伍:

class TwilightBus:"""讓乘客銷聲匿跡的校車"""def __init__(self, passenegers = None):if passenegers is None:self.passengers = []else:self.passengers = list(passenegers)def pick(self, name):self.passengers.append(name)def drop(self, name):# 這里我針對傳入的參數列表的第二個元素做了修改,與上面的代碼不同# 上面的代碼是self.passengers.remove(name),其他的代碼都是一樣的self.passengers[1].remove(name)basketball_team = ['Sue', ['Tina', 'Maya', 'Diana', 'Pat']] bus = TwilightBus(basketball_team) bus.drop('Tina') print(basketball_team)

運行結果為['Sue', ['Maya', 'Diana', 'Pat']]。
可以看到,因為公交車座位不足,隊員Tina去了別的公交車,而方法直接將隊員Tina開除出了籃球隊。當然,drop的代碼是我專門針對basketball_team這個列表編寫的,目的是為了演示。那么為了不使Tina因為車位不足就被開除出隊,我們就要解決問題,問題的根源仍然是對象共享的問題,為了完完全全使basketball_team與self.passengers隔絕開來,就需要使用內置模塊copy中的deepcopy()方法來完成這個任務(具體內容參見于上一篇:
第八章《對象引用、可變性和垃圾回收》(上)中的8.3 默認做淺復制):

# 在此之前要導入copy模塊 import copydef __init__(self, passenegers = None):if passenegers is None:self.passengers = []else:# 這里就使用了深復制,這個操作可以使passengers與self.passengers完全獨立self.passengers = copy.deepcopy(passenegers)

這樣一來,上面的代碼就不會將Tina開除出隊了。

8.5 del 和垃圾回收

del語句刪除名稱,而不是對象。僅當刪除的變量保留的是對象的最后一個引用,或者無法得到對象時,,del命令可能導致對象被當做垃圾回收。另外,重新綁定也可能會導致對象的引用數量歸零,導致對象被銷毀。
在Cpython中,垃圾回收使用的主要算法是引用計數。實際上,每個對象都會統計有多少個引用指向自己,當引用計數歸零時,對象會被立即銷毀:Cpython會在對象上調用__del__方法,然后釋放分配給對象的內存。python的其他實現有更復雜的垃圾回收程序,且不依賴與引用計數,這意味著,對象引用計數歸零時可能不會立即調用__del__方法。

>>> import weakref >>> s1 = {1,2,3} >>> s2 = s1 # s1和s2是別名,指向同一個集合:{1,2,3} >>> def bye(): # 這個函數一定不能是要銷毀對象的綁定方法,否則會有一個指向對象的引用 ... print("Gone with the wind...") >>> ender = weakref.finalize(s1, bye) # 在s1引用的對象上注冊bye回調 >>> ender.alive # 調用finalize對象之前,.alive屬性的值為True True >>> del s1 >>> ender.alive # del不刪除對象,而是刪除對象的引用 True >>> s2 = 'spam' # 重新綁定最后一個引用s2,讓{1,2,3}無法獲取。對象被銷毀了,調用了bye回調,ender.alive變為False Gone with the wind... >>> ender.alive False

為什么{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 True

weakref.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也是 True

str、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

總結

以上是生活随笔為你收集整理的第八章《对象引用、可变性和垃圾回收》(下)的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。