一等公民指的是 Python 的函數可以動態建立,能賦值給別的變量,能做爲參傳給函數,也能做爲函數的返回值。總而言之,函數和普通變量並無什麼區別。python
函數是一等公民,這是函數式編程的基礎,然而 Python 中基本上不會使用 lambda 表達式,由於在 lambda 表達式的中僅能使用單純的表達式,不能賦值,不能使用 while、try 等語句,所以 lambda 表達式要麼難以閱讀,要麼根本沒法寫出。這極大的限制了 lambda 表達式的使用場景。程序員
上文說過,函數和普通變量沒什麼區別,但普通變量並非函數,由於這些變量沒法調用。但若是某個類實現了 __call__
這個魔術方法,這個類的實例就均可以像函數同樣被調用:編程
class Person: def __init__(self): self.name = 'bestswifter' self.age = 22 self.sex = 'm' def __call__(self): print(self) def __str__(self): return 'Name: {user.name}, Age: {user.age}, Sex: {user.sex}'.format(user=self) p = Person() p() # 等價於 print(p)
對於熟悉 C 系列語言的人來講,函數傳參的方式一目瞭然。默認是拷貝傳值,若是傳指針是引用傳值。咱們先來看一段簡單的 Python 代碼:swift
def foo(arg): arg = 5 print(arg) a = 1 foo(a) print(a) # 輸出 5 和 1
這段代碼的結果符合咱們的預期,從這段代碼來看,Python 也屬於拷貝傳值。但若是再看這段代碼:設計模式
def foo(arg): arg.append(1) print(arg) a = [1] foo(a) print(a) # 輸出兩個 [1, 1]
你會發現參數數組在函數內部被改變了。就像是 C 語言中傳遞了變量的指針同樣。因此 Python 究竟是拷貝傳值仍是引用傳值呢?答案都是否認的!
Python 的傳值方式能夠被理解爲混合傳值。對於那些不可變的對象(好比 1.1.2 節中介紹過的元組,還有數字、字符串類型),傳值方式是拷貝傳值;對於那些可變對象(好比數組和字典)則是引用傳值。數組
Python 的函數能夠有默認值,這個功能很好用:緩存
def foo(a, l=[]): l.append(a) return l foo(2,[1]) # 給數組 [1] 添加一個元素 2,獲得 [1,2] foo(2) # 沒有傳入數組,使用默認的空數組,獲得 [2]
然而若是這樣調用:閉包
foo(2) # 利用默認參數,獲得 [2] foo(3) # 居然獲得了 [2, 3]
函數調用了兩次之後,默認參數被改變了,也就是說函數調用產生了反作用。這是由於默認參數的存儲並不像函數裏的臨時變量同樣存儲在棧上、隨着函數調用結束而釋放,而是存儲在函數這個對象的內部:app
foo.__defaults__ # 一開始確實是空數組 foo(2) # 利用默認參數,獲得 [2] foo.__defaults__ # 若是打印出來看,已經變成 [2] 了 foo(3) # 再添加一個元素就獲得了 [2, 3]
由於函數 foo
做爲一個對象,不會被釋放,所以這個對象內部的屬性也不會隨着屢次調用而自動重置,會一直保持上次發生的變化。基於這個前提,咱們得出一個結論:函數的默認參數不容許是可變對象,好比這裏的 foo
函數須要這麼寫:框架
def foo(a, l=None): if l is None: l = [] l.append(a) return l print(foo(2)) # 獲得 [2] print(foo(3)) # 獲得 [3]
如今,給參數添加默認值的行爲在函數體中完成,不會隨着函數的屢次調用而累積。
對於 Python 的默認參數來講:
若是默認值是不可變的,能夠直接設置默認值,不然要設置爲 None 並在函數體中設置默認值。
當參數個數不肯定時,能夠在參數名前加一個 *: def foo(*args): print(args) foo(1, 2, 3) # 輸出 [1, 2, 3] 若是直接把數組做爲參數傳入,它實際上是單個參數,若是要把數組中全部元素都做爲單獨的參數傳入,則在數組前面加上 *: a = [1, 2, 3] foo(a) # 會輸出 ([1,2,3], ) 由於只傳了一個數組做爲參數 foo(*a) # 輸出 [1, 2, 3] 這裏的單個 * 只能接收非關鍵字參數,也就是僅有參數值的哪些參數。若是想接受關鍵字參數,須要用 ** 來表示: def foo(*args, **kwargs): print(args) print(kwargs) foo(1,2,3, a=61, b=62) # 第一行輸出:[1, 2, 3] # 第二行輸出:{'a': 61, 'b': 62} 相似的,字典變量傳入函數只能做爲單個參數,若是要想展開並被 **kwargs 識別,須要在字典前面加上兩個星號 **: a = [1, 2, 3] d = {'a': 61, 'b': 62} foo(*a, **d)
2.2.4 參數分類
Python 中函數的參數能夠分爲兩大類:
定位參數(Positional):表示參數的位置是固定的。好比對於函數 foo(a, b) 來講,foo(1, 2) 和 foo(2, 1) 就是大相徑庭的,a 和
b 的位置是固定的,不可隨意調換。 關鍵詞參數(Keyword):表示參數的位置不重要,可是參數名稱很重要。好比 foo(a = 1, b = 2) 和 foo(b = 2, a = 1) 的含義相同。
有一種參數叫作僅限關鍵字(Keyword-Only)參數,好比考慮這個函數:
def foo(*args, n=1, **kwargs): print(n)
這個函數在調用時,若是參數 n 不指定名字,就會被前面的 *args
處理掉,若是指定的名字不是 n,又會被後面的 **kwargs
處理掉,因此參數 n 必須精確的以 (n = xxx)
的形式出現,也就是 Keyworld-Only。
在 2.2.2 節中,咱們查看了函數變量的 __defaults__
屬性,其實這就是一種內省,也就是在運行時動態的查看變量的信息。
前文說過,函數也是對象,所以函數的變量個數,變量類型都應該有辦法獲取到,若是你須要開發一個框架,也許會對函數有各類奇葩的檢查和校驗。
如下面這個函數爲例: g = 1 def foo(m, *args, n, **kwargs): a = 1 b = 2 首先能夠獲取函數名,函數所在模塊的全局變量等: foo.__globals__ # 全局變量,包含了 g = 1 foo.__name__ # foo 咱們還能夠看到函數的參數,函數內部的局部變量: foo.__code__.co_varnames # ('m', 'n', 'args', 'kwargs', 'a', 'b') foo.__code__.co_argcount # 只計算參數個數,不考慮可變參數,因此獲得 2 或者用 inspect 模塊來查看更詳細的信息: import inspect sig = inspect.signature(foo) # 獲取函數簽名 sig.parameters['m'].kind # POSITIONAL_OR_KEYWORD 表示能夠是定位參數或關鍵字參數 sig.parameters['args'].kind # VAR_POSITIONAL 定位參數構成的數組 sig.parameters['n'].kind # KEYWORD_ONLY 僅限關鍵字參數 sig.parameters['kwargs'].kind # VAR_KEYWORD 關鍵字參數構成的字典 inspect.getfullargspec(foo) # 獲得:ArgSpec(args=['m', 'n'], varargs='args', keywords='kwargs', defaults=None)
本節的新 API 比較多,但並不要求記住這些 API 的用法。再次強調,本文的寫做目的是爲了創建讀者對 Python 的整體
認知,瞭解 Python 能作什麼,至於怎麼作,那是文檔該作的事。
經典的設計模式有 23 個,雖然設計模式都是經常使用代碼的總結,理論上來講與語法無關。但不得不認可的是,標準的設計模
式在不一樣的語言中,有的由於語法的限制根本沒法輕易實現(好比在 C 語言中實現組合模式),有的則由於語言的特定功能
,變得冗餘囉嗦。
以策略模式爲例,有一個抽象的策略類,定義了策略的接口,而後使用者選擇一個具體的策略類,構造他們的實例而且調
用策略方法。具體代碼能夠參考:策略模式在百度百科的定義。
然而這些對象自己並無做用,它們僅僅是能夠調用相同的方法而已,只不過在 Java 中,全部的任務都須要由對象來完成。
即便策略自己就是一個函數,但也必須把它包裹在一個策略對象中。因此在 Python 中更優雅寫法是直接把策略函數做爲變量使用。
不過這就引入一個問題,如何判斷某個函數是個策略呢,畢竟在面向對象的寫法中,只要檢查它的父類是不是抽象的策略類便可。
也許你已經見過相似的寫法:
strategy def strategyA(n): print(n * 2)
下面就開始介紹裝飾器。
首先,裝飾器是個函數,它的參數是被裝飾的函數,返回值也是一個函數: def decorate(origin_func): # 這個參數是被裝飾的函數 print(1) # 先輸出點東西 return origin_func # 把原函數直接返回 @decorate # 注意這裏不是函數調用,因此不用加括號,也不用加被修飾的函數名 def sayHello(): print('Hello') sayHello() # 若是沒有裝飾器,只會打印 'Hello',實際結果是打印 1 再打印 'Hello' 所以,使用裝飾器的這種寫法: @decorate def foo(): pass 和下面這種寫法是徹底等價的, 初學者能夠把裝飾器在心中默默的轉換成下一種寫法,以方便理解: def foo(): pass foo = decorate(foo)
須要注意的是,裝飾器函數 decorate
在模塊被導入時就會執行,而被裝飾的函數只在被調用時纔會執行,也就是說即便不調用 sayHello
函數也會輸出 1,但這樣就不會輸出 Hello 了。
有了裝飾器,配合前面介紹的函數對象,函數內省,咱們能夠作不少有意思的事,至少判斷上一節中某個函數是不是策
略是很是容易的。在裝飾器中,咱們還能夠把策略函數都保存到數組中, 而後提供一個「推薦最佳策略」的功能, 其實就
是遍歷執行全部的策略,而後選擇最好的結果。
上一節中的裝飾器主要是爲了介紹工做原理,它的功能很是簡單,並不會改變被裝飾函數的運行結果,僅僅是在導入時
裝飾函數,而後輸出一些內容。換句話說,即便不執行函數,也要執行裝飾器中的 print
語句,並且由於直接返回函數
的緣故,其實沒有真正的起到裝飾的效果。
如何作到裝飾時不輸出任何內容,僅在函數執行最初輸出一些東西呢?這是常見的 AOP(面向切片編程) 的需求。這就
要求咱們不能再直接返回被裝飾的函數,而是應該返回一個新的函數,因此新的裝飾器須要這麼寫:
def decorate(origin_func): def new_func(): print(1) origin_func() return new_func @decorate def sayHello(): print('Hello') sayHello() # 運行結果不變,可是僅在調用函數 sayHello 時纔會輸出 1
這個例子的工做原理是,sayHello
函數做爲參數 origin_func
被傳到裝飾器中,通過裝飾之後,它實際上變成了 new_func
,會先輸出 1 再執行原來的函數,也就是 sayHello
。
這個例子很簡陋,由於咱們知道了 sayHello
函數沒有參數,因此才能定義一個一樣沒有參數的替代者:nwe_func
。若是咱們在開發一個框架,要求裝飾器能對任意函數生效,就須要用到 2.2.3 中介紹的 *
和 **
這種不定參數語法了。
若是查看 sayHello 函數的名字,獲得的結果將是 new_func: sayHello.__name__ # new_func 這是很天然的,由於本質上其實執行的是: new_func = decorate(sayHello) 而裝飾器的返回結果是另外一個函數 new_func,二者僅僅是運行結果相似,但兩個對象並無什麼關聯。 因此爲了處理不定參數,而且不改變被裝飾函數的外觀(好比函數名),咱們須要作一些細微的修補工做。這些工做都是模板代碼,因此 Python 早就提供了封裝: import functools def decorate(origin_func): @functools.wraps(origin_func) # 這是 Python 內置的裝飾器 def new_func(*args, **kwargs): print(1) origin_func(*args, **kwargs) return new_func
在 2.4.2 節的代碼註釋中我解釋過,裝飾器後面不要加括號,被裝飾的函數自動做爲參數,傳遞到裝飾器函數中。若是加了
括號和參數,就變成手動調用裝飾器函數了,大多數時候這與預期不符(由於裝飾器的參數通常都是被裝飾的函數)。
不過裝飾器能夠接受自定義的參數,而後返回另外一個裝飾器,這樣外面的裝飾器實際上就是一個裝飾器工廠,能夠根據用戶
的參數,生成不一樣的裝飾器。仍是以上面的裝飾器爲例,我但願輸出的內容不是固定的 1,而是用戶能夠指定的,代碼就應該這麼寫:
import functools def decorate(content): # 這實際上是一個裝飾器工廠 def real_decorator(origin_func): # 這纔是剛剛的裝飾器 @functools.wraps(origin_func) def new_func(): print('You said ' + str(content)) # 如今輸出內容能夠由用戶指定 origin_func() return new_func # 在裝飾器裏,返回的是新的函數 return real_decorator # 裝飾器工廠返回的是裝飾器 裝飾器工廠和裝飾器的區別在於它能夠接受參數,返回一個裝飾器: @decorate(2017) def sayHello(): print('Hello') sayHello() 其實等價於: real_decorator = decorate(2017) # 經過裝飾器工廠生成裝飾器 new_func = real_decorator(sayHello) # 正常的裝飾器工做邏輯 new_func() # 調用的是裝飾過的函數
C 語言中咱們定義變量用到的語法是:
int a = 1;
這背後的含義是定義了一個 int
類型的變量 a
,至關於申請了一個名爲 a
的盒子(存儲空間),裏面裝了數字 1。
而後咱們改變 a
的值:a = 2;
,能夠打印 a
的地址來證實它並無發生變化。因此只是盒子裏裝的內容(指針指向的位置)
發生了改變:
可是在 Python 中,變量不是盒子。好比一樣的定義變量:
a = 1
這裏就不能把 a
理解爲 int
類型的變量了。由於在 Python 中,變量沒有類型,值纔有,或者說只有對象纔有類型。
由於即便是數字 1,也是 int
類的實例,而變量 a
更像是給這個對象貼的一個標籤。
若是執行賦值語句 a = 2
,至關於把標籤 a 貼在另外一個對象上:
基於這個認知,咱們如今應該更容易理解 2.2.1 節中所說的函數傳參規則了。若是傳入的是不可變類型,好比 int
,改變它的值實際上就是把標籤掛在新的對象上,天然不會改變原來的參數。若是是可變類型,而且作了修改,那麼函數中的變量和外面的變量都是指向同一個對象的標籤,因此會共享變化。
根據上一節的描述,直接把變量賦值給另外一個變量, 還算不上覆制: a = [1, 2, 3] b = a b == a # True,等同性校驗,會調用 __eq__ 函數,這裏只判斷內容是否相等 b is a # True,一致性校驗,會檢查是不是同一個對象,調用 hash() 函數,能夠理解爲比較指針 可見不只僅數組相同,就連變量也是相同的,能夠把 b 理解爲 a 的別名。 若是用切片,或者數組的構造函數來建立新的數組,獲得的是原數組的淺拷貝: a = [1, 2, 3] b = list(a) b == a # True,由於數組內容相同 b is a # False,如今 a 和 b 是兩個變量,剛好指向同一個數組對象 但若是數組中的元素是可變的,能夠看到這些元素並無被徹底拷貝: a = [[1], [2], [3]] b = list(a) b[0].append(2) a # 獲得 [[1, 2], [2], [3]],由於 a[0] 和 b[0] 其實仍是掛在相同對象上的不一樣標籤 若是想要深拷貝,須要使用 copy 模塊的 deepcopy 函數: import copy b = copy.deepcopy(a) b[0].append(2) a # 變成了 [[1, 2], [2], [3]] a # 仍是 [[1], [2], [3]]
此時,不只僅是每一個元素的引用被拷貝,就連每一個元素本身也被拷貝。因此如今的 a[0]
和 b[0]
是指向兩個不一樣對象的兩個不一樣變量(標籤),天然就互不干擾了。
若是要實現自定義對象的深複製,只要實現 __deepcopy__
函數便可。這個概念在幾乎全部面向對象的語言中都會存在,就不詳細介紹了。
Python 內存管理使用垃圾回收的方式,當沒有指向對象的引用時,對象就會被回收。然而對象一直被持有也並不是什
麼好事,好比咱們要實現一個緩存,預期目標是緩存中的內容隨着真正對象的存在而存在,隨着真正對象的消失而
消失。若是由於緩存的存在,致使被緩存的對象沒法釋放,就會致使內存泄漏。
Python 提供了語言級別的支持,咱們可使用 weakref
模塊,它提供了 weakref.WeakValueDictionary
這
個弱引用字典來確保字典中的值不會被引用。若是想要獲取某個對象的弱引用,可使用 weakref.ref(obj)
函數。
靜態函數其實和類的方法沒什麼關係,它只是剛好定義在類的內部而已,因此這裏我用函數(function) 來形容它。它能夠沒有參數: class Person: @staticmethod # 用 staticmethod 這個修飾器來代表函數是靜態的 def sayHello(): print('Hello') Person.sayHello() # 輸出 'Hello` 靜態函數的調用方式是類名加上函數名。類方法的調用方式也是這樣,惟一的不一樣是須要用 @staticmethod 修飾器,並且方法的第一個參數必須是類: class Person: @classmethod # 用 classmethod 這個修飾器來代表這是一個類方法 def sayHi(cls): print('Hi: ' + cls.__name__) Person.sayHi() # 輸出 'Hi: Person`
類方法和靜態函數的調用方法一致,在定義時除了修飾器不同,惟一的區別就是類方法須要多聲明一個參數。
這樣看起來比較麻煩,但靜態函數沒法引用到類對象,天然就沒法訪問類的任何屬性。
因而問題來了,靜態函數有何意義呢?有的人說類名能夠提供命名空間的概念,但在我看來這種解釋並不成立,
由於每一個 Python 文件均可以做爲模塊被別的模塊引用,把靜態函數從類裏抽取出來,定義成全局函數,也是有命名空間的:
# 在 module1.py 文件中: def global(): pass class Util: @staticmethod def helper(): pass # 在 module2.py 文件中: import module1 module1.global() # 調用全局函數 module1.Util.helper() # 調用靜態函數
從這個角度看,定義在類中的靜態函數不只不具有命名空間的優勢,甚至調用語法還更加囉嗦。對此,個人理解是:靜
態函數能夠被繼承、重寫,但全局函數不行,因爲 Python 中的函數是一等公民,所以不少時候用函數替代類都會使代碼
更加簡潔,但缺點就是沒法繼承,後面還會有更多這樣的例子。
Python (等多數動態語言)中的類並不像 C/OC/Java 這些靜態語言同樣,須要預先定義屬性。咱們能夠直接在初始化
函數中建立屬性:
class Person: def __init__(self, name): self.name = name bs = Person('bestswifter') bs.name # 值是 'bestswifter' 因爲 __init__ 函數是運行時調用的,因此咱們能夠直接給對象添加屬性: bs.age = 22 bs.age # 由於剛剛賦值了,因此如今取到的值是 22
若是訪問一個不存在的屬性,將會拋出異常。從以上特性來看,對象其實和字典很是類似,但這種過於靈活的特性其實蘊含了潛在的
風險。好比某個封裝好的父類中定義了許多屬性, 可是子類的使用者並不必定清楚這一點,他們極可能會不當心就重寫了父類的屬性
。一種隱藏並保護屬性的方式是在屬性前面加上兩個下劃線:
class Person: def __init__(self): self.__name = 'bestswifter' bs = Person() bs.__name # 這樣是沒法獲取屬性的 bs._Person__name # 這樣仍是能夠讀取屬性 這是由於 Python 會自動處理以雙下劃線開頭的屬性,把他們重名爲 _Classname__attrname 的格式。因爲 Python
對象的全部屬性都保存在實例的 __dict__ 屬性中,咱們能夠驗證一下: bs = Person() bs.__dict__ # 獲得 {'_Person__name': 'bestswifter'}
但不少人並不承認經過名稱改寫(name mangling) 的方式來存儲私有屬性,緣由很簡單,只要知道改寫規則,依然很容易的就能讀寫
私有屬性。與其自欺欺人,不如採用更簡單,更通用的方法,好比給私有屬性前面加上單個下劃線 _
。
注意,以單個下劃線開頭的屬性不會觸發任何操做,徹底靠自覺與共識。任何稍有追求的 Python 程序員,都不該該讀寫這些屬性。
使用過別的面嚮對象語言的讀者應該都清楚屬性的 getter
和 setter
函數的重要性。它們封裝了屬性的讀寫操做,
能夠添加一些額外的邏輯,好比校驗新值,返回屬性前作一些修飾等等。最簡陋的 getter
和 setter
就是兩個普通函數:
class Person: def get_name(self): return self.name.upper() def set_name(self, new_name): if isinstance(new_name, str): self.name = new_name.lower() def __init__(self, name): self.name = name bs = Person('bestswifter') bs.get_name() # 獲得大寫的名字: 'BESTSWIFTER' bs.set_name(1) # 因爲新的名字不是字符串,因此沒法賦值 bs.get_name() # 仍是老的名字: 'BESTSWIFTER' 工做雖然完成了,但方法並不高明。在 1.2.3 節中咱們就見識到了 Python 的一個特色:「內部高度封裝,徹底對外透明」。
這裏手動調用 getter 和 setter 方法顯得有些愚蠢、囉嗦,好比對比下面的兩種寫法,在變量名和函數名很長的狀況下,差距會更大: bs.name += '1995' bs.set_name(bs.get_name() + '1995') Python 提供了 @property 關鍵字來裝飾 getter 和 setter 方法,這樣的好處是能夠直接使用點語法,瞭解 Objective-C
的讀者對這一特性必定倍感親切: class Person: @property # 定義 getter def name(self): # 函數名就是點語法訪問的屬性名 return self._name.upper() # 如今真正的屬性是 _name 了 @name.setter # 定義 setter def name(self, new_name): # 函數名不變 if isinstance(new_name, str): self._name = new_name.lower() # 把值存到私有屬性 _name 裏 def __init__(self, name): self.name = name bs = Person('bestswifter') bs.name # 其實調用了 name 函數,獲得大寫的名字: 'BESTSWIFTER' bs.name = 1 # 其實調用了 name 函數,由於類型不符,沒法賦值 bs.name # 仍是老的名字: 'BESTSWIFTER' 咱們已經在 2.4 節詳細學習了裝飾器,應該能意識到這裏的 @property 和 @xxx.setter 都是裝飾器。所以上述寫法實際上等價於: class Person: def get_name(self): return self._name.upper() def set_name(self, new_name): if isinstance(new_name, str): self._name = new_name.lower() # 以上是老舊的 getter 和 setter 定義 # 若是不用 @property,能夠定義一個 property 類的實例 name = property(get_name, set_name) 可見,特性的本質是給類建立了一個類屬性,它是 property 類的實例,構造方法中須要把 getter、setter 等函數傳入,
咱們能夠打印一下類的 name 屬性來證實: Person.name # <property object at 0x107c99868> 理解特性的工做原理相當重要。以這裏的 name 特性爲例,咱們訪問了對象的 name 屬性,可是它並不存在,因此會嘗試訪問類
的 name 屬性,這個屬性是 property 類的實例,會對讀寫操做作特殊處理。這也意味着,若是咱們重寫了類的 name 屬性,
那麼對象的讀寫方法就不會生效了: bs = Person() Person.name = 'hello' bs.name # 實例並無 name 屬性,所以會訪問到類的屬性 name,如今的值是 'hello` 了 若是訪問不存在的屬性,默認會拋出異常,但若是實現了 __getattr__ 函數,還有一次挽救的機會: class Person: def __getattr__(self, attr): return 0 def __init__(self, name): self.name = name bs = Person('bestswifter') bs.name # 直接訪問屬性 bs.age # 獲得 0,這是 __getattr__ 方法提供的默認值 bs.age = 1 # 動態給屬性賦值 bs.age # 獲得 1,注意!!!這時候就不會再調用 __getattr__ 方法了 因爲 __getattr__ 只是兜底策略,處理一些異常狀況,並不是每次都能被調用,因此不能把重要的業務邏輯寫在這個方法中。
在上一節中,咱們利用特性來封裝 getter
和 setter
,對外暴露統一的讀寫接口。但有些 getter
和 setter
的邏輯實際上是能夠複用的,好比商品的價格和剩餘數量在賦值時,都必須是大於 0 的數字。這時候若是每次都要寫一遍 setter
,代碼就顯得很冗餘,因此咱們須要一個能批量生產特性的函數。因爲咱們已經知道了特性是 property
類的實例,並且是類的屬性,因此代碼能夠這樣寫:
def quantity(storage_name): # 定義 getter 和 setter def qty_getter(instance): return instance.__dict__[storage_name] def qty_setter(instance, value): if value > 0: # 把值保存在實例的 __dict__ 字典中 instance.__dict__[storage_name] = value else: raise ValueError('value must be > 0') return property(qty_getter, qty_setter) # 返回 property 的實例
有了這個特性工廠,咱們能夠這樣來定義特性:
class Item: price = quantity('price') number = quantity('number') def __init__(self): pass i = Item() i.price = -1 # Traceback (most recent call last): # ... # ValueError: value must be > 0
做爲追求簡潔的程序員,咱們不由會問,在 price = quantity('price')
這行代碼中,屬性名重複了兩次,能不能在 quantity
函數中自動讀取左邊的屬性名呢,這樣代碼就能夠簡化成 price = quantity()
了。
答案顯然是否認的,由於右邊的函數先被調用,而後才能把結果賦值給左邊的變量。不過咱們能夠採用迂迴策略,變相的實現上面的需求:
def quantity(): try: quantity.count += 1 except AttributeError: quantity.count = 0 storage_name = '_{}:{}'.format('quantity', quantity.count) def qty_getter(instance): return instance.__dict__[storage_name] def qty_setter(instance, value): if value > 0: instance.__dict__[storage_name] = value else: raise ValueError('value must be > 0') return property(qty_getter, qty_setter)
這段代碼中咱們利用了兩個技巧。首先函數是一等公民, 因此函數也是對象,天然就有屬性。因此咱們利用 try ... except
很容易的就給函數工廠添加了一個計數器對象 count
,它每次調用都會增長,而後再拼接成存儲時用的鍵 storage_name
,而且能夠保證不一樣 property
實例的存儲鍵名各不相同。
其次,storage_name
在 getter
和 setter
函數中都被引用到,而這兩個函數又被 property
的實例引用,因此 storage_name
會由於被持有而延長生命週期。這也正是閉包的一大特性:可以捕獲自由變量並延長它的生命週期和做用域。
咱們來驗證一下:
class Item: price = quantity() number = quantity() def __init__(self): pass i = Item() i.price = 1 i.number = 2 i.price # 獲得 1,能夠正常訪問 i.number # 獲得 2,能夠正常訪問 i.__dict__ # {'_quantity:0': 1, '_quantity:1': 2}
可見如今存儲的鍵名能夠被正確地自動生成。
文件描述符的做用和特性工廠同樣,都是爲了批量的應用特性。它的寫法也和特性工廠很是相似:
class Quantity: def __init__(self, storage_name): self.storage = storage_name def __get__(self, instance, owner): return instance.__dict__[self.storage] def __set__(self, instance, value): if value > 0: instance.__dict__[self.storage] = value else: raise ValueError('value must be > 0')
主要有如下幾個改動:
property
類的實例了,所以 getter
和 setter
方法的名字是固定的,這樣才能知足協議。__get__
方法的第一個參數是描述符類 Quantity
的實例,第二個參數 self
是要讀取屬性的實例,好比上面的 i
,也被稱做託管實例。第三個參數是託管類,也就是 Item
。__set__
方法的前兩個參數含義相似,第三個則是要讀取的屬性名,好比 price
。和特性工廠相似,屬性描述符也能夠實現 storage_name
的自動生成,這裏就不重複代碼了。看起來屬性描述符和特性工廠幾乎同樣,但因爲屬性描述符是類,它就能夠繼承。好比這裏的 Quantity
描述符有兩個功能:自動存儲和值的校驗。自動存儲是一個很是通用的邏輯,而值的校驗是可變的業務邏輯,因此咱們能夠先定義一個 AutoStorage
描述符來實現自動存儲功能,而後留下一個空的 validate
函數交給子類去重寫。
而特性工廠做爲函數,天然就沒有上述功能,這二者的區別相似於 3.2.1 節中介紹的靜態函數與全局函數的區別。
咱們知道類的屬性都會存儲在 __dict__
字典中,即便沒有顯式的給屬性賦值,但只要字典裏面有這個字段,也是能夠讀取到的:
class Person: pass p = Person() p.__dict__['name'] = 'bestswifter' p.name # 不會報錯,而是返回字典中的值,'bestswifter'
但咱們在特性工廠和屬性描述符的實現中,都是直接把屬性的值存儲在 __dict__
中,並且鍵就是屬性名。以前咱們還介紹過,特性的工做原理是沒有直接訪問實例的屬性,而是讀取了 property
的實例。那直接把值存在 __dict__
中,會不會致使特性失效,直接訪問到原始內容呢?從以前的實踐結果來看,答案是否認的,要解釋這個問題,咱們須要搞明白訪問實例屬性的查找順序。
假設有這麼一段代碼:
o = cls() # 假設 o 是 cls 類的實例
o.attr # 試圖訪問 o 的屬性 attr
再對上一節中的屬性描述符作一個簡單的分類:
覆蓋型描述符:定義了 __set__ 方法的描述符
非覆蓋型描述符:沒有定義 __set__ 方法的描述符
在執行 o.attr 時,查找順序以下:
若是 attr 出如今 cls 或父類的 __dict__ 中,且 attr 是覆蓋型描述符,那麼調用 __get__ 方法。
不然,若是 attr 出如今 o 的__dict__ 中,返回 o.__dict__[attr]
不然,若是attr 出如今 cls 或父類的 __dict__ 中,若是 attr 是非覆蓋型描述符,那麼調用 __get__ 方法。
不然,若是沒有非覆蓋型描述符,直接返回 cls.__dict__[attr]
不然,若是 cls 實現了 __getattr__ 方法,調用這個方法
拋出 AttributeError
因此,在訪問類的屬性時,覆蓋型描述符的優先級是高於直接存儲在 __dict__
中的值的。
本節內容部分摘自個人這篇文章:從 Swift 的面向協議編程說開去,本節聊的是多繼承在 Python 中的知識,若是想閱讀關於多繼承的討論,請參考原文。
不少語言類的書籍都會介紹,多繼承是個危險的行爲。誠然,狹義上的多繼承在絕大多數狀況下都是不合理的。這裏所謂的 「狹義」,指的是一個類擁有多個父類。咱們要明確一個概念:繼承的目的不是代碼複用,而是聲明一種 is a
的關係,代碼複用只是 is a
關係的一種外在表現。
所以,若是你須要狹義上的多繼承,仍是應該先問問本身,真的存在這麼多 is a
的關係麼?你是須要聲明這種關係,仍是爲了代碼複用。若是是後者,有不少更優雅的解決方案,由於多繼承的一個直接問題就是菱形問題(Diamond Problem)。
可是廣義上的多繼承是必須的,不能由於懼怕多繼承的問題就忽略多繼承的優勢。廣義多繼承 指的是經過定義接口(Interface)以及接口方法的默認實現,造成「一個父類,多個接口」的模式,最終實現代碼的複用。固然,不是每一個語言都有接口的概念,好比 Python 裏面叫 Mixin,會在 3.3.3 節中介紹。
廣義上的多繼承很是常見,有一些教科書式的例子,好比動物能夠按照哺乳動物,爬行動物等分類,也能夠按照有沒有翅膀來分類。某一個具體的動物可能知足上述好幾類。在實際的開發中也處處都是廣義多繼承的使用場景,好比 iOS 或者安卓開發中,系統控件的父類都是固定的,若是想讓他們複用別的父類的代碼,就會比較麻煩。