《流暢的Python》筆記本篇是「面向對象慣用方法」的第一篇,一共六篇。本篇主要是一些概念性的討論,內容有:Python中的變量,對象標識,值,別名,元組的某些特性,深淺複製,引用,函數參數,垃圾回收,del命令,弱引用等,比較枯燥,但卻能解決程序中不易察覺的bug。html
先用一個形象的比喻來講明Python中的變量:變量是標註而不是盒子。也就是說,Python中的變量更像C++中的引用,最能說明這一點的就是多個變量指向同一個列表,但也有例外,在遇到某些內置類型,好比字符串str
時,變量則變成了「盒子」:python
# 代碼1 >>> a = [1, 2] >>> b = a # 標註,引用 >>> a.append(3) >>> b [1, 2, 3] >>> c = "c" >>> d = c # 「盒子」 >>> c = "cc" >>> d 'c'
補充:說到了賦值方式,Python和C++同樣,也是等號右邊先執行。算法
用一個更學術的詞來替換「標註」,那就是「別名」。在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
比==
快。app
元組和大多數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
這同時也說明,並非每一個元組都是可散列的!函數
複製對象時,相等性和標識之間的區別有更深刻的影響。副本與源對象相等,但ID不一樣。而若是對象內部還有其餘對象,這就涉及到了深淺複製的問題:究竟是複製內部對象呢仍是共享內部對象?
對列表和其餘可變序列來講,咱們可使用構造方法或[:]
來建立副本。然而,這兩種方法作的都是淺複製,它們只複製了最外層的容器,副本中的元素是源容器中元素的引用。若是全部元素都是不可變的,那這樣作沒問題,還能節省內存;但若是其中有可變元素,這麼作就可能出問題:
# 代碼5 l1 = [3, [11, 22], (7, 8)] l2 = list(l1) # <1> l1.append(100) l1[1].remove(22) print("l1:", l1, "\nl2:", l2) l2[1] += [33, 44] # <2> l2[2] += (10, 11) # <3> print("l1:", l1, "\nl2:", 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點須要解釋:
l1[1]
和l2[1]
指向同一列表,l1[2]
和l2[2]
指向同一元組。由於是淺複製,只是複製引用;+=
運算對可變對象來講是就地運算,不會建立新對象,因此對兩個列表都有影響;+=
運算對元組這樣的不可變對象來講,等同於l2[2] = l2[2] + (10, 11)
,此操做隱式地建立了新對象,l2[2]
從新綁定到了新對象,因此只有列表l2[2]
發生了改變,而l1[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, "\nl2:", l2, "\nl3:", 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模塊的官方文檔。
經過別名共享對象還能解釋Python中傳遞參數的方式,以及使用可變類型做爲參數默認值引發的問題。
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指向了新的元組
不要使用可變類型做爲參數的默認值!其實這個問題在以前的文章「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
變成了用戶傳入的參數的別名),則用戶傳入的參數在運行過程當中可能會被修改,而這並不必定是用戶想要的,這便違反了"最少驚訝原則"(竟然還真有這麼個原則)
對象毫不會自行銷燬;然而,沒法獲得對象時,可能會被當作垃圾回收。——Python語言參考手冊
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 False
不知道你們看到上述代碼第15行時會不會產生以下疑惑:第8行代碼明明把s1
引用傳給了finalize
函數(爲了監控對象和調用回調,必需要有引用),那麼對象{1, 2, 3}
則應該至少有三個引用,可爲何最後它仍是被銷燬了呢?這就牽扯到了弱引用這個概念。
弱引用不會妨礙所指對象被當作垃圾回收,即弱引用不會增長對象的引用計數。(弱引用常被用於緩存,但具體用在緩存的哪些地方目前筆者還不清楚.....)
弱引用仍是可調用對象,下面的代碼展現瞭如何使用weakref.ref
實例獲取所指對象。
補充在代碼以前:Python控制檯會自動把結果不爲None
的表達式的結果綁定到變量_
(下劃線)上。這也說明了一個問題:微觀管理內存時,隱式賦值會爲對象建立新引用,而這有可能會致使一些意外結果。
# 代碼14 >>> import weakref >>> a_set = {1, 2} # 對象{1, 2}的引用數+1 >>> wref = weakref.ref(a_set) # 並無增長所指對象的引用數 >>> wref <weakref at 0x0000013D739E2D18; to 'set' at 0x0000013D739BE588> >>> 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
類型的類屬性,保存實例的弱引用。
weakref
集合以及通常的弱引用,能處理的對象類型有限:
基本的list
和dict
實例不能做爲弱引用的所指對象,但它們的子類則能夠;
class MyList(list): """MyList的實例可做爲弱引用的所指對象"""
set
的實例可做爲所指對象;int
和tuple
的實例不能做爲弱引用的所指對象,它們的子類也不行。但這些侷限基本上是CPython的實現細節,其餘Python解釋器的狀況可能不一樣。
本節內容是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
!)
每一個Python對象都有標識、類型和值,只有對象的值可能變化。
變量保存的是引用,這對Python編程有不少實際的影響:
+=
或*=
等運算符來講,若是左邊的變量綁定了不可變對象,則會建立新對象,而後從新綁定;若是是可變對象,則就地修改;==
用於比較值,is
用於比較引用。某些狀況下,可能須要保存對象的引用,但不留存對象自己,好比記錄某個類的全部實例,這能夠用弱引用解決。
迎你們關注個人微信公衆號"代碼港" & 我的網站 www.vpointer.net ~