摘要:函數的參數作為引用時唯一支持的參數傳遞模式是共享傳參,它指函數的形參獲得實參中各個引用的副本,即形參是實參的別名。而在上面這個例子中,類的屬性實際上是形參所指向的對象所指對象,的別名。
《流暢的Python》筆記1. 變量、標識、相等性和別名本篇是“面向對象慣用方法”的第一篇,一共六篇。本篇主要是一些概念性的討論,內容有:Python中的變量,對象標識,值,別名,元組的某些特性,深淺復制,引用,函數參數,垃圾回收,del命令,弱引用等,比較枯燥,但卻能解決程序中不易察覺的bug。
先用一個形象的比喻來說明Python中的變量:變量是標注而不是盒子。也就是說,Python中的變量更像C++中的引用,最能說明這一點的就是多個變量指向同一個列表,但也有例外,在遇到某些內置類型,比如字符串str時,變量則變成了“盒子”:
# 代碼1 >>> a = [1, 2] >>> b = a # 標注,引用 >>> a.append(3) >>> b [1, 2, 3] >>> c = "c" >>> d = c # “盒子” >>> c = "cc" >>> d "c"
補充:說到了賦值方式,Python和C++一樣,也是等號右邊先執行。
1.1 相等性( == )與標識( is )用一個更學術的詞來替換“標注”,那就是“別名”。在C++中,引用就是變量的別名,Python中也是,比如代碼1中的變量b就是變量a的別名,但如果是以下形式,變量b則不是a的別名:
# 代碼2 >>> a = [1, 2] >>> b = [1, 2] >>> a == b # a和b的值相等 True >>> a is b # a和b分別綁定了不同的對象,雖然對象的值相等 False
==檢測對象的值是否相等,is運算符檢測對象的標識(ID)是否相等,id()返回對象標識的整數表示。一般判斷兩對象的標識是否相等并不直接使用id(),更多的是使用is運算符。
對象ID在不同的實現中有所不同:在CPython中,id()返回對象的內存地址,但在其他Python解釋器中可能是別的值。但不管怎么,對象的ID一定唯一,且在生命周期中保持不變。
通常我們關心的是值,而不是標識,所以==出現的頻率比is高。但在變量和單例值之間比較時,應該使用is。目前,最常使用is檢測變量綁定的值是不是None,推薦的寫法是:
# 代碼3 x is None # 并非 x == None x is not None # 并非 x != None
is運算符比==速度快,因為它不能重載,所以Python不用尋找并調用特殊方法,而是直接比較兩個對象的ID。a == b其實是語法糖,實際調用a.__eq__(b)。雖然繼承自object的__eq__方法也是比較對象的ID,結果和is一樣,但大多數內置類型覆蓋了該方法,處理過程更復雜,這就是為什么is比==快。
1.2 元組的相對不可變性元組和大多數Python集合一樣,保存的是對象的引用。元組的不可變性其實是指tuple數據結構的物理內容(即保存的引用)不可變,與引用的對象無關。如果引用的對象可變,即便元組本身不可變,元素依然可變,不變的是元素的標識:
# 代碼4 >>> t1 = (1, 2, [30, 40]) >>> t2 = (1, 2, [30, 40]) >>> t1 == t2 True >>> id(t1[-1]) 2019589413704 >>> t1[-1].append(99) >>> t1 (1, 2, [30, 40, 99]) >>> id(t1[-1]) # 內容變了,標識沒有變 2019589413704 >>> t1 == t2 False
這同時也說明,并不是每個元組都是可散列的!
2.深淺復制復制對象時,相等性和標識之間的區別有更深入的影響。副本與源對象相等,但ID不同。而如果對象內部還有其他對象,這就涉及到了深淺復制的問題:到底是復制內部對象呢還是共享內部對象?
2.1 默認做淺復制對列表和其他可變序列來說,我們可以使用構造方法或[:]來創建副本。然而,這兩種方法做的都是淺復制,它們只復制了最外層的容器,副本中的元素是源容器中元素的引用。如果所有元素都是不可變的,那這樣做沒問題,還能節省內存;但如果其中有可變元素,這么做就可能出問題:
# 代碼5 l1 = [3, [11, 22], (7, 8)] l2 = list(l1) # <1> l1.append(100) l1[1].remove(22) print("l1:", l1, " l2:", l2) l2[1] += [33, 44] # <2> l2[2] += (10, 11) # <3> print("l1:", l1, " l2:", l2) # 結果 l1: [3, [11], (7, 8), 100] # 追加元素只影響了l1 l2: [3, [11], (7, 8)] # 但刪除l1[1]中的元素影響了兩個列表 l1: [3, [11, 33, 44], (7, 8), 100] # +=對可變對象是就地操作,影響了兩個列表 l2: [3, [11, 33, 44], (7, 8, 10, 11)] # +=對不可變對象會創建新對象,只影響了l2
以上代碼有3點需要解釋:
<1>:l1[1]和l2[1]指向同一列表,l1[2]和l2[2]指向同一元組。因為是淺復制,只是復制引用;
<2>:+=運算對可變對象來說是就地運算,不會創建新對象,所以對兩個列表都有影響;
<3>:+=運算對元組這樣的不可變對象來說,等同于l2[2] = l2[2] + (10, 11),此操作隱式地創建了新對象,l2[2]重新綁定到了新對象,所以只有列表l2[2]發生了改變,而l1[2]沒有改變。
2.2 為任意對象做深復制和淺復制淺復制并非是一種錯誤,只是一種選擇。而有時我們需要的是深復制,即副本不共享內部對象的引用。copy模塊提供的deepcopy和copy函數能為任意對象做深復制和淺復制。
# 代碼6 import copy l1 = [3, [11, 22]] l2 = copy.copy(l1) # 淺復制 l3 = copy.deepcopy(l1) # 深復制 l1[1].append(33) # 影響了l2,但沒有影響l3 print("l1:", l1, " l2:", l2, " l3:", l3) # 結果 l1: [3, [11, 22, 33]] l2: [3, [11, 22, 33]] l3: [3, [11, 22]]
在做深復制時,如果對象之間有循環引用,樸素的深復制算法(換句話說就是你自己寫的深復制算法)很可能會陷入無限循環,然后報錯。deepcopy會記住已經復制的對象,而不會進入無限循環:
# 代碼7 >>> a = [10, 20] >>> b = [a, 30] # 包含a的引用 >>> b [[10, 20], 30] >>> a.append(b) # 相互引用 >>> a [10, 20, [[...], 30]] >>> a[2][0] [10, 20, [[...], 30]] >>> a[2][0][2][0] [10, 20, [[...], 30]] >>> from copy import deepcopy >>> c = deepcopy(a) # 不會報錯,能正確處理相互引用的問題 >>> c [10, 20, [[...], 30]]
此外,深復制有時可能太深了。例如,對象可能會引用不該復制的外部資源或單例值,這時,深復制就不應該復制這些值。如果要控制copy和deepcopy的行為,我們可以在對象中重寫特殊方法__copy__和__deepcopy__,具體內容這里就不展開了,大家可以參考copy模塊的官方文檔。
3. 函數參數通過別名共享對象還能解釋Python中傳遞參數的方式,以及使用可變類型作為參數默認值引起的問題。
3.1 函數的參數作為引用時Python唯一支持的參數傳遞模式是共享傳參(call by sharing),它指函數的形參獲得實參中各個引用的副本,即形參是實參的別名。這種方案的結果就是,函數可能會修改作為參數傳入的可變對象,但無法修改這些對象的標識(不能把一個對象替換成另一個對象):
# 代碼8 def f(a, b): a += b return a x, y = 1, 2 print(f(x, y), x, y) a, b = [1, 2], [3, 4] print(f(a, b), a, b) t, u = (10, 20), (30, 40) print(f(t, u), t, u) # 結果 3 1 2 # x, y是不可變對象,沒有影響到x, y [1, 2, 3, 4] [1, 2, 3, 4] [3, 4] # x是可變對象,影響到了x (10, 20, 30, 40) (10, 20) (30, 40) # x沒有指向新的元組,但形參a指向了新的元組3.2 參數默認值
不要使用可變類型作為參數的默認值!其實這個問題在之前的文章“Python學習之路7-函數”的2.3小節中有所提及。現在我們來看下面這個例子:
首先定義一個類:
# 代碼9 class Bus: def __init__(self, passengers=[]): # 默認值是個可變對象 self.passengers = passengers def pick(self, name): self.passengers.append(name) def drop(self, name): self.passengers.remove(name)
下面是這個類的行為:
# 代碼10 >>> bus1 = Bus(["Alice", "Bill"]) # 直到第8行Bus的表現都是正常的 >>> bus1.passengers ["Alice", "Bill"] >>> bus1.pick("Charlie") >>> bus1.drop("Alice") >>> bus1.passengers ["Bill", "Charlie"] >>> bus2 = Bus() # 使用默認值 >>> bus2.pick("Carrie") >>> bus2.passengers ["Carrie"] # 到目前為止也是正常的 >>> bus3 = Bus() # 也是用默認值 >>> bus3.passengers ["Carrie"] # 不正常了! >>> bus3.pick("Dave") >>> bus2.passengers ["Carrie", "Dave"] # bus2的值也被改變了 >>> bus2.passengers is bus3.passengers # 這倆是同一對象的別名 True >>> bus1.passengers # bus1依然正常 ["Bill", "Charlie"]
上述行為的原因在于,參數的默認值在導入模塊時計算,方法或函數的形參指向這個默認值。而在上面這個例子中,類的屬性self.passengers實際上是形參passengers所指向的對象(所指對象,referent)的別名。而bus1行為正常是因為從一開始它的passengers就沒有指向默認值。
這里有點像單例模式:參數的默認值是唯一的,只要采用默認值,不管創建多少個Bus的實例,它們的self.passengers都是同一個空列表[]對象的別名,不會為每一個實例多帶帶創建一個專屬的[]。
運行上述代碼之后,可以查看Bus.__init__對象的__defaults__屬性,它存儲了參數的默認值:
# 代碼11 >>> Bus.__init__.__defaults__ (["Carrie", "Dave"],) >>> Bus.__init__.__defaults__[0] is bus2.passengers # self.passengers就是一個別名! True
這也說明了為什么要用None作為接收可變值的參數的默認值:
# 代碼12 class Bus: def __init__(self, passengers=None): # 默認值是個可變對象 if passengers is None: # 并不推薦 if passengers == None 這種寫法 self.passengers = [] else: self.passengers = list(passengers) # 注意這里! -- snip --
代碼12中的第7行并不是直接把形參passengers賦值給self.passengers,而是形參的副本(這里是淺復制)。如果直接賦值,即self.passengers = passengers(self.passengers變成了用戶傳入的參數的別名),則用戶傳入的參數在運行過程中可能會被修改,而這并不一定是用戶想要的,這便違反了"最少驚訝原則"(居然還真有這么個原則)
4. del和垃圾回收對象絕不會自行銷毀;然而,無法得到對象時,可能會被當做垃圾回收?!狿ython語言參考手冊
del語句刪除變量(即"引用"),而不是對象。del命令可能導致對象被當做垃圾回收,但這僅發生在當刪除的變量保存的是對象的最后一個引用,或者無法得到對象時(如果兩個對象相互引用,如代碼7,當它們的引用只存在二者之間時,垃圾回收程序會判定它們都無法獲取,進而把它們都銷毀)。重新綁定也可能會導致對象的引用數量歸零,進而對象被銷毀。
在CPython中,垃圾回收使用的主要算法是引用計數。實際上,每個對象都會統計有多少個引用指向自己。當引用計數歸零時,對象立即被銷毀。但在其他Python解釋器中則不一定是引用計數算法。
補充:有個__del__特殊方法,它不是用來銷毀實例的,而是在實例被銷毀前用來執行一些最后的操作,比如釋放外部資源等。我們不應該在代碼中調用它,Python解釋器會在銷毀實例時先調用它(如果定義了),然后再釋放內存。它相當于C++中的析構函數。
我們可以使用weakref.finalize來演示對象被銷毀時的情況:
# 代碼13 >>> import weakref >>> s1 = {1, 2, 3} >>> s2 = s1 >>> def bye(): # 它充當一個回調函數 ... print("Gone with the wind...") # 一定不要傳入待銷毀對象的綁定方法,否則會有一個指向對象的引用 >>> ender = weakref.finalize(s1, bye) # 在s1引用的對象上注冊bye回調 >>> ender.alive True >>> del s1 >>> ender.alive True # 說明 del s1并沒有刪除對象 >>> s2 = "spam" Gone with the wind... # 引用計數為零,對象被刪除 >>> ender.alive False5. 弱引用
不知道大家看到上述代碼第15行時會不會產生如下疑惑:第8行代碼明明把s1引用傳給了finalize函數(為了監控對象和調用回調,必須要有引用),那么對象{1, 2, 3}則應該至少有三個引用,可為什么最后它還是被銷毀了呢?這就牽扯到了弱引用這個概念。
5.1 weakref.ref弱引用不會妨礙所指對象被當做垃圾回收,即弱引用不會增加對象的引用計數。(弱引用常被用于緩存,但具體用在緩存的哪些地方目前筆者還不清楚.....)
弱引用還是可調用對象,下面的代碼展示了如何使用weakref.ref實例獲取所指對象。
補充在代碼之前:Python控制臺會自動把結果不為None的表達式的結果綁定到變量_(下劃線)上。這也說明了一個問題:微觀管理內存時,隱式賦值會為對象創建新引用,而這有可能會導致一些意外結果。
# 代碼14 >>> import weakref >>> a_set = {1, 2} # 對象{1, 2}的引用數+1 >>> wref = weakref.ref(a_set) # 并沒有增加所指對象的引用數 >>> wref5.2 weakref集合>>> wref() # 弱引用是個可調用對象 {1, 2} # 發生了隱式賦值,變量 _ 指向了對象{1, 2},引用數+1 >>> a_set = {2, 3} # 引用數 -1 >>> wref() # 所指對象依然存在,還沒有被銷毀 {1, 2} >>> wref() is None # 此時所指對象依然存在 False # 變量 _ 指向了對象False,對象{1, 2}引用數歸零,銷毀 >>> wref() is None # 驗證所指對象已被銷毀 True
weakref.ref類其實是底層接口,供高級用途使用,一般程序最好使用werakref集合和finalize函數,即最好使用WeakKeyDictionary、WeakValueDictionary、WeakSet和finalize(它們在內部使用弱引用),不推薦自己動手創建并處理weakref.ref實例,除非你的工作就是專門和這些東西打交道的。
WeakValueDictionary類實現的是一種可變映射,里面的值("鍵值對"中的"值",而不是字典中的"值")是對象的弱引用。被引用的對象在程序中的其他地方被當做垃圾回收后,對應的鍵會自動從WeakValueDictionary中刪除。因此,它經常用于緩存。(查看緩存中變量是否依然存在?給框架用?)
# 代碼15 >>> import weakref >>> class Cheese: ... def __init__(self, kind): ... self.kind = kind ... >>> stock = weakref.WeakValueDictionary() >>> catalog = [Cheese("Red Leicester"), Cheese("Parmesan")] >>> for cheese in catalog: ... stock[cheese.kind] = cheese ... >>> sorted(stock.keys()) ["Red Leicester", "Parmesan"] # 表現正常 >>> del catalog >>> sorted(stock.keys()) ["Parmesan"] # 這是怎么回事? >>> del cheese # 這是問題所在 >>> sorted(stock.keys()) []
臨時變量引用了對象,這可能會導致該變量的存在時間比預期長。通常,這對局部變量來說不是問題,因為它們在函數返回時會被銷毀。但上述代碼中,for循環中的變量cheese是全局變量,除非顯示刪除,否則不會消失。
與WeakValueDictionary對應的是WeakKeyDictionary,后者的鍵是弱引用,它的一些可能用途如下:
它的實例可以為應用中其他部分擁有的對象附加數據,這樣就無需為對象添加屬性。這對屬性訪問受限的對象尤其有用。
WeakSet類的用途則很簡單:"保存元素弱引用的集合。當某元素沒有強引用時,集合會把它刪除。"如果一個類需要知道它的所有實例,一種好的方案是創建一個WeakSet類型的類屬性,保存實例的弱引用。
5.3 弱引用的局限weakref集合以及一般的弱引用,能處理的對象類型有限:
基本的list和dict實例不能作為弱引用的所指對象,但它們的子類則可以;
class MyList(list): """MyList的實例可作為弱引用的所指對象"""
set的實例可作為所指對象;
自定義類的實例可以;
int和tuple的實例不能作為弱引用的所指對象,它們的子類也不行。
但這些局限基本上是CPython的實現細節,其他Python解釋器的情況可能不同。
6. CPython對不可變類型走的捷徑本節內容是Python實現的細節,可以跳過。
這些細節是CPython核心開發者走的捷徑和優化措施,利用這些細節寫的代碼在其他Python解釋器中可能沒用,在CPython未來的版本中也可能沒用。下面是具體內容:
對元組t來說,t[:]和tuple(t)不創建副本,而是返回同一個對象的引用;
str、bytes和frozenset實例也是如此,并且frozenset的copy方法返回的也不是副本(注意,frozenset的實例fs不能用fs[:],因為fs不是序列);
str的實例還有共享字符串字面量的行為:
>>> s1 = "ABC" >>> s2 = "ABC" >>> s1 is s2 True
這叫做"駐留"(interning),這是一種優化措施。CPython還會在小的整數上使用這種優化,防止重復創建常用數字,如0,-1。但CPython不會駐留所有字符串和數字,駐留的條件是實現細節,而且沒有文檔說明。所以千萬不要依賴這個特性!(比較字符串或數字請用==,而不是is!)
7. 總結每個Python對象都有標識、類型和值,只有對象的值可能變化。
變量保存的是引用,這對Python編程有很多實際的影響:
簡單的賦值不會創建副本;
對+=或*=等運算符來說,如果左邊的變量綁定了不可變對象,則會創建新對象,然后重新綁定;如果是可變對象,則就地修改;
對現有的變量賦予新值不會修改之前綁定的對象。這叫重新綁定:現有變量綁定了其它對象。如果變量是之前那個對象的最后一個引用,該對象會被回收;
函數的參數以別名的形式傳遞,這意味著,函數可能會修改通過參數傳入的可變對象。這一行為無法避免,除非在函數內部創建副本,或者使用不可變對象;
不要使用可變類型作為函數的默認值!
==用于比較值,is用于比較引用。
某些情況下,可能需要保存對象的引用,但不留存對象本身,比如記錄某個類的所有實例,這可以用弱引用解決。
迎大家關注我的微信公眾號"代碼港" & 個人網站 www.vpointer.net ~
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/41836.html
摘要:一對象引用基礎知識變量是標注而不是容器。也就是說元組中不可變的是元素的標識,但元組的值會隨著引用的可變對象變化而變化。在中每個對象的引用都會有統計。弱引用不會妨礙對象被當做垃圾回收。 導語:本文章記錄了本人在學習Python基礎之面向對象篇的重點知識及個人心得,打算入門Python的朋友們可以來一起學習并交流。 本文重點: 1、明確變量保存的是引用這一本質;2、熟悉對象引用的基礎知識;...
摘要:每個變量都有標識類型和值對象一旦創建它的標識絕不會變標識可以簡單的理解為對象在內存中的地址別名跟是別名指向如果增加新的內容也會增加相等性為運算符比較連個對象的值對象中保存的數據標識為因為他們都指向這個列表比較對象的標識元組相對不可變性元組保 a = [1,2,3,4] b = a 每個變量都有標識,類型和值.對象一旦創建,它的標識絕不會變;標識可以簡單的理解為對象在內存中的地址. ...
摘要:里,有兩種方法獲得一定范圍內的數字返回一個列表,還有返回一個迭代器。在引用計數的基礎上,還可以通過標記清除解決容器對象可能產生的循環引用的問題。列舉常見的內置函數的作用,過濾函數,循環函數累積函數一行代碼實現乘法表。 showImg(https://segmentfault.com/img/remote/1460000019294205); 1、為什么學習Python? 人生苦短?人間...
摘要:運算符比較兩個對象的標識函數返回對象標識的整數表示。實際上,每個對象都會統計有多少引用指向自己。對象被銷毀了,調用了回調,的值變成了。當對象的引用數量歸零后,垃圾回收程序會把對象銷毀。引用的目標對象稱為所指對象。 對象不是個盒子 showImg(https://segmentfault.com/img/bV95mW?w=1784&h=988); class Gizmo: def...
摘要:對象引用和可變性變量不是盒子,而是便利貼變量的賦值方式比如是將一個變量分配給一個對象比如整數。運算符比較兩個對象的標識函數返回對象標識的整數表示。每個對象都會統計有多少引用指向自己。對象被銷毀執行回調函數輸出 對象引用和可變性 變量不是盒子,而是‘便利貼’ >>> a = [1,2,3] >>> b = a >>> a.append(5) >>> a [1, 2, 3, 5] >>> ...
閱讀 535·2019-08-30 15:55
閱讀 944·2019-08-29 15:35
閱讀 1199·2019-08-29 13:48
閱讀 1910·2019-08-26 13:29
閱讀 2933·2019-08-23 18:26
閱讀 1237·2019-08-23 18:20
閱讀 2834·2019-08-23 16:43
閱讀 2710·2019-08-23 15:58