Python 簡單入門指北(二)

Python 簡單入門指北(二)

2 函數

2.1 函數是一等公民

一等公民指的是 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)

2.2 函數參數

2.2.1 函數傳參

對於熟悉 C 系列語言的人來講,函數傳參的方式一目瞭然。默認是拷貝傳值,若是傳指針是引用傳值。咱們先來看一段簡單的 Python 代碼:swift

 
 
def foo(arg):
    arg = 5
    print(arg)
    
a = 1
foo(a)
print(a)
# 輸出 51

這段代碼的結果符合咱們的預期,從這段代碼來看,Python 也屬於拷貝傳值。但若是再看這段代碼:設計模式

 
 
def foo(arg):
    arg.append(1)
    print(arg)
    
a = [1]
foo(a)
print(a) # 輸出兩個 [1, 1]
你會發現參數數組在函數內部被改變了。就像是 C 語言中傳遞了變量的指針同樣。因此 Python 究竟是拷貝傳值仍是引用傳值呢?答案都是否認的

Python 的傳值方式能夠被理解爲混合傳值。對於那些不可變的對象(好比 1.1.2 節中介紹過的元組,還有數字、字符串類型),傳值方式是拷貝傳值;對於那些可變對象(好比數組和字典)則是引用傳值。數組

2.2.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 並在函數體中設置默認值。

2.2.3 多參數傳遞

當參數個數不肯定時,能夠在參數名前加一個 *:

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 中函數的參數能夠分爲兩大類:

  1. 定位參數(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.3 函數內省

在 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 能作什麼,至於怎麼作,那是文檔該作的事。

2.4 裝飾器

2.4.1 設計模式的消亡

經典的設計模式有 23 個,雖然設計模式都是經常使用代碼的總結,理論上來講與語法無關。但不得不認可的是,標準的設計模

式在不一樣的語言中,有的由於語法的限制根本沒法輕易實現(好比在 C 語言中實現組合模式),有的則由於語言的特定功能

,變得冗餘囉嗦。

以策略模式爲例,有一個抽象的策略類,定義了策略的接口,而後使用者選擇一個具體的策略類,構造他們的實例而且調

用策略方法。具體代碼能夠參考:策略模式在百度百科的定義

然而這些對象自己並無做用,它們僅僅是能夠調用相同的方法而已,只不過在 Java 中,全部的任務都須要由對象來完成。

即便策略自己就是一個函數,但也必須把它包裹在一個策略對象中。因此在 Python 中更優雅寫法是直接把策略函數做爲變量使用。

不過這就引入一個問題,如何判斷某個函數是個策略呢,畢竟在面向對象的寫法中,只要檢查它的父類是不是抽象的策略類便可。

也許你已經見過相似的寫法:

 
 
strategy
def strategyA(n):
    print(n * 2)

下面就開始介紹裝飾器。

2.4.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 了。

有了裝飾器,配合前面介紹的函數對象,函數內省,咱們能夠作不少有意思的事,至少判斷上一節中某個函數是不是策

略是很是容易的。在裝飾器中,咱們還能夠把策略函數都保存到數組中, 而後提供一個「推薦最佳策略」的功能, 其實就

是遍歷執行全部的策略,而後選擇最好的結果。

2.4.3 裝飾器進階

上一節中的裝飾器主要是爲了介紹工做原理,它的功能很是簡單,並不會改變被裝飾函數的運行結果,僅僅是在導入時

裝飾函數,而後輸出一些內容。換句話說,即便不執行函數,也要執行裝飾器中的 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.4 裝飾器工廠

在 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()                           # 調用的是裝飾過的函數

3 面向對象

3.1 對象內存管理

3.1.1 對象不是盒子

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,改變它的值實際上就是把標籤掛在新的對象上,天然不會改變原來的參數。若是是可變類型,而且作了修改,那麼函數中的變量和外面的變量都是指向同一個對象的標籤,因此會共享變化。

3.1.2 默認淺複製

 
 
根據上一節的描述,直接把變量賦值給另外一個變量, 還算不上覆制:

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__ 函數便可。這個概念在幾乎全部面向對象的語言中都會存在,就不詳細介紹了。

3.1.3 弱引用

Python 內存管理使用垃圾回收的方式,當沒有指向對象的引用時,對象就會被回收。然而對象一直被持有也並不是什

麼好事,好比咱們要實現一個緩存,預期目標是緩存中的內容隨着真正對象的存在而存在,隨着真正對象的消失而

消失。若是由於緩存的存在,致使被緩存的對象沒法釋放,就會致使內存泄漏。

Python 提供了語言級別的支持,咱們可使用 weakref 模塊,它提供了 weakref.WeakValueDictionary

個弱引用字典來確保字典中的值不會被引用。若是想要獲取某個對象的弱引用,可使用 weakref.ref(obj) 函數。

3.2 Python 風格的對象

3.2.1 靜態函數與類方法

 
 
靜態函數其實和類的方法沒什麼關係,它只是剛好定義在類的內部而已,因此這裏我用函數(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 中的函數是一等公民,所以不少時候用函數替代類都會使代碼

更加簡潔,但缺點就是沒法繼承,後面還會有更多這樣的例子。

3.2.2 屬性 attribute

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 程序員,都不該該讀寫這些屬性。

3.2.3 特性 property

使用過別的面嚮對象語言的讀者應該都清楚屬性的 gettersetter 函數的重要性。它們封裝了屬性的讀寫操做,

能夠添加一些額外的邏輯,好比校驗新值,返回屬性前作一些修飾等等。最簡陋的 gettersetter 就是兩個普通函數:

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__ 只是兜底策略,處理一些異常狀況,並不是每次都能被調用,因此不能把重要的業務邏輯寫在這個方法中。

 

3.2.4 特性工廠

在上一節中,咱們利用特性來封裝 gettersetter,對外暴露統一的讀寫接口。但有些 gettersetter 的邏輯實際上是能夠複用的,好比商品的價格和剩餘數量在賦值時,都必須是大於 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_namegettersetter 函數中都被引用到,而這兩個函數又被 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}
可見如今存儲的鍵名能夠被正確地自動生成。

3.2.5 屬性描述符

文件描述符的做用和特性工廠同樣,都是爲了批量的應用特性。它的寫法也和特性工廠很是相似:

 
 
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')

主要有如下幾個改動:

  1. 不用返回 property 類的實例了,所以 gettersetter 方法的名字是固定的,這樣才能知足協議。
  2. __get__ 方法的第一個參數是描述符類 Quantity 的實例,第二個參數 self 是要讀取屬性的實例,好比上面的 i,也被稱做託管實例。第三個參數是託管類,也就是 Item
  3. __set__ 方法的前兩個參數含義相似,第三個則是要讀取的屬性名,好比 price

和特性工廠相似,屬性描述符也能夠實現 storage_name 的自動生成,這裏就不重複代碼了。看起來屬性描述符和特性工廠幾乎同樣,但因爲屬性描述符是類,它就能夠繼承。好比這裏的 Quantity 描述符有兩個功能:自動存儲和值的校驗。自動存儲是一個很是通用的邏輯,而值的校驗是可變的業務邏輯,因此咱們能夠先定義一個 AutoStorage 描述符來實現自動存儲功能,而後留下一個空的 validate 函數交給子類去重寫。

而特性工廠做爲函數,天然就沒有上述功能,這二者的區別相似於 3.2.1 節中介紹的靜態函數與全局函數的區別。

3.2.6 實例屬性的查找順序

咱們知道類的屬性都會存儲在 __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__ 中的值的。

3.3 多繼承

本節內容部分摘自個人這篇文章:從 Swift 的面向協議編程說開去,本節聊的是多繼承在 Python 中的知識,若是想閱讀關於多繼承的討論,請參考原文。

3.3.1 多繼承的必要性

不少語言類的書籍都會介紹,多繼承是個危險的行爲。誠然,狹義上的多繼承在絕大多數狀況下都是不合理的。這裏所謂的 「狹義」,指的是一個類擁有多個父類。咱們要明確一個概念:繼承的目的不是代碼複用,而是聲明一種 is a 的關係,代碼複用只是 is a 關係的一種外在表現。

所以,若是你須要狹義上的多繼承,仍是應該先問問本身,真的存在這麼多 is a 的關係麼?你是須要聲明這種關係,仍是爲了代碼複用。若是是後者,有不少更優雅的解決方案,由於多繼承的一個直接問題就是菱形問題(Diamond Problem)。

可是廣義上的多繼承是必須的,不能由於懼怕多繼承的問題就忽略多繼承的優勢。廣義多繼承 指的是經過定義接口(Interface)以及接口方法的默認實現,造成「一個父類,多個接口」的模式,最終實現代碼的複用。固然,不是每一個語言都有接口的概念,好比 Python 裏面叫 Mixin,會在 3.3.3 節中介紹。

廣義上的多繼承很是常見,有一些教科書式的例子,好比動物能夠按照哺乳動物,爬行動物等分類,也能夠按照有沒有翅膀來分類。某一個具體的動物可能知足上述好幾類。在實際的開發中也處處都是廣義多繼承的使用場景,好比 iOS 或者安卓開發中,系統控件的父類都是固定的,若是想讓他們複用別的父類的代碼,就會比較麻煩。

相關文章
相關標籤/搜索