19. 再說函式~那些不得不知道的事兒

大牧莫邪發表於2017-05-19

前面的課程中,我們已經對函式有了簡單的瞭解 函式的宣告、函式的的呼叫、函式的引數以及返回值等等

本節內容主要對函式中的一些高階操作進行講解,方便大家在專案操作過程中對函式的操作更加靈活一些 * 函式遞迴 * 函式變數賦值 * 引數中的函式 * 匿名函式 * 返回值中的函式:閉包 * 偏函式 * 裝飾器

1. 函式遞迴

函式的遞迴,就是讓在函式的內部呼叫函式自身的情況,這個函式就是遞迴函式。 遞迴函式其實是另外一種意義的迴圈 如:計算一個數字的階乘操作,將這個功能封裝成函式fact(num) 提示:階乘演算法是按照小於等於當前數字的自然數進行乘法運算 計算5的階乘:5 X 4 X 3 X 2 X1 計算n的階乘:n X (n - 1) X ... X 3 X 2 X 1 ```

# 定義一個遞迴函式
def fact(num):
    if n == 1:
        return n
    return n * fact(n - 1)
# 執行函式
>>> fact(1)
1
>>> fact(2)
2
>>> fact(3)
6
>>> fact(4)
24
>>> fact(5)
120
>>> fact(9)
362880

```

遞迴操作,整個計算過程如下 計算5的階乘:fact(5)

-> fact(5)

->5 X fact(5 - 1)

->5 X (4 X fact(4 - 1))

->5 X (4 X (3 X fact(3 - 1)))

->5 X (4 X (3 X (2 X fact(2 - 1)))))

=>5 X (4 X (3 X (2 X 1)))

=>5 X (4 X (3 X 2))

=>5 X (4 X 6)

=>5 X 24

=>120

我們在之前說過,遞迴就是另外一種特殊的迴圈:函式級別的迴圈 所以遞迴函式也可以使用迴圈來進行實現 但是迴圈的實現思路沒有遞迴清晰。

使用遞迴函式時一定需要注意:遞迴函式如果一旦執行的層數過多就會導致記憶體溢位程式崩潰。

有一種做法是將遞迴函式的返回值中,不要新增表示式,而是直接返回一個函式,這樣的做法旨在進行尾遞迴優化,大家如果有興趣的話可以上網自行查詢一下;由於不同的直譯器對於函式遞迴執行的不同的處理,所以遞迴的使用請慎重分析和操作。

2. 函式變數賦值

函式,是一種操作行為 函式名稱,其實是這種操作行為賦值的變數 呼叫函式,其實是通過這個賦值的變數加上一堆圓括號來進行函式的執行 ```

# 定義了一個函式,函式命名為printMsg
def printMsg (msg):
    print("you say :" + msg)
# 通過變數printMsg來進行函式的呼叫
printMsg("my name is jerry!")
```
既然函式名稱只是一個變數,變數中存放了這樣的一個函式物件
我們就可以將函式賦值給另一個變數
```
# 將函式賦值給變數pm
pm = printMsg;
# 就可以通過pm來進行函式的執行了
pm(" my name is tom!")

```

3. 引數中的函式

函式作為一個物件,我們同樣可以將函式當成一個實際引數傳遞給另一個函式進行處理 ```

# 系統內建求絕對值函式abs(),賦值給變數f
f = abs;
# 定義一個函式,用於獲取兩個資料絕對值的和
def absSum(num1, num2, fn):
    return fn(num1) + fn(num2)
# 呼叫執行函式
res = absSum(-3, 3, f)
# 執行結果
~ 6

```

函式作為引數進行傳遞,極大程度的擴充套件了函式的功能,在實際操作過程中有非常廣泛的應用。

4. 匿名函式

在一個函式的引數中,需要另一個函式作為引數進行執行: ```

def printMsg(name, fn):
    print(name)
    fn()

常規做法是我們定義好自己的函式,然後將函式名稱傳遞給引數進行呼叫

def f():
    print("日誌記錄:函式執行完成")
printMsg("jerry", f)

```

重點在這裡

我們通過如下的方式來呼叫函式 ```

printName("tom", lambda:print("函式執行完成..."))
# 執行結果
tom
函式執行完成

``` 在printName函式呼叫時,需要一個函式作為引數的地方,出現了lambda這樣一個詞彙和後面緊跟的語句

lambda是一種表示式,一種通過表示式來實現簡單函式操作的形式,lambda表示式可以看成是一種匿名函式 常規的lambda表示式的語法結構是 ```

lambda 引數列表:執行程式碼

如下面這樣的lambda表示式

lambda x, y: x * y
# 就是定義了類似如下的程式碼:
def test(x, y):
    x * y

```

lambda表示式已經在後端開發的各種語言中出現了,以其簡潔的風格和靈活的操作風靡一時,但是需要注意,lambda表示式簡化了簡單函式的定義,但是同時也降低了程式碼的可讀性 所以這樣的lambda表示式,可以使用,但是要慎重使用,切記不能濫用,否則造成非常嚴重的後果:你的程式碼由於極差的可讀性就會變成一次性的!

5. 返回值中的函式:閉包

函式作為物件,同樣也可以出現在返回值中,其實就是在函式中又定義了另外的函式 在一個函式中定義並使用其他的函式,這樣的方式在不同的程式語言中有不同的管理方式,在Python中,這樣的方式也成為閉包。 ```

# 在一個函式outerFn中定義了一個函式innerFn
def outerFn():
    x = 12;
    def innerFn():
        x = x *12
    return innerFn;
# 執行函式
f = outerFn();
f()
# 執行結果:144

# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
# 什麼是閉包,閉包就是在函式A中新增定義了另一個函式B
# 最後將函式B返回,通過函式B就可以直接使用區域性變數,擴大了區域性變數的作用域
# 
# 為什麼要使用閉包,閉包就是為了再多人協同開發專案過程中,同時會有多個人寫多
# 個python檔案並且要互相引入去使用,此時如果不同的開發人員定義的全域性變數出現
# 名稱相同,就會出現變數值覆蓋引起的資料汙染,也稱為變數的全域性汙染。為了避免
# 出現這樣的情況,我們通常通過閉包來管理當前檔案中變數的使用。
#
# 怎麼使用閉包,閉包函式中可以定義其他的任意多個變數和函式,在閉包函式執行的
# 時候這些函式都會執行,也就是將函式的執行從程式載入執行->遷移->閉包函式執行的
# 過程
# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

```

6. 偏函式

常規函式操作中,我們在函式的引數中可以新增引數的預設值來簡化函式的操作,偏函式也可以做到這一點,偏函式可以在一定程度上更加方便的管理我們的函式操作 偏函式通過內建模組functools的partial()函式進行定義和處理

如之前我們學習過的一個型別轉換函式int(str),用於將一個字串型別的數字轉換成整數,同樣的,可以在型別轉換函式中指定將一個字串型別的數字按照指定的進位制的方式進行轉換

```

# 將一個字串型別的123轉換成整數型別的123
int("123")  # 123
# 將一個字串12按照16進位制轉換成十進位制的整數
int("12", base=16)  # 18
# 將一個字串17按照8進位制轉換成十進位制的整數
int("17", base=8)  15
# 將一個字串1110按照2進位制轉換成十進位制的整數
int("1110", base=2) 14

# 注意:上述要轉換的字串的整數必須滿足對應的進位制,否則會轉換報錯
# 按照八進位制轉換,但是要轉換的字串中的數字不是8進位制數字
int("9", base=8)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 8: '9'
# 按照2進位制轉換,但是要轉換的字串不是2進位制數字
int("3", base=2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 2: '3'

```

上述這樣的操作方式,通過一個命名關鍵字引數base的方式來指定轉換進位制,可讀性較好,但是操作起來稍顯繁瑣,我們可以通過functools的partial()函式進行改造如下: ```

# functools.partial()函式語法結構
新函式名稱 = functools.partial(函式名稱, 預設賦值引數)

通過對指定的函式進行引數的預設賦值,然後將這樣的一個新的函式儲存在變數中,通過這個新函式就可以執行更加簡潔的操作了

# 原始的2進位制資料轉換
int("111", base=2)
~執行結果:7
# 引入我們需要的模組functools
import functools
# 通過偏函式擴充套件一個新的函式int2
int2 = functools.partial(int, base=2)
# 使用新的函式,新的函式等價於上面的int("111", base=2)
int2("111")
~執行結果:7

系統內建函式可以通過上述的形式進行封裝,那麼我們自定義函式是否也可以封裝呢

# 定義一個函式,可以根據使用者輸入的型別來遍歷資料
def showData(data, *, type=1):
    if type == 1:     #列印字串
        print(data)
    elif type ==2:   # 遍歷列表、元組、集合
        for x in data:
            print(x)
    elif type == 3: # 遍歷字典
        for k, v in data.items():
            print(k, v)
# 列印字串
showData("hello functools partial");
# 列印列表
showData([1,2,3,4,5], type=2)
# 列印元組
showData((1,2,3,4,5), type=2)
# 列印集合
showData({1,2,3,4,5}, type=2)
# 列印字典
showData({"1":"a", "2":"b", "3":"c"}, type=3)

# 使用偏函式進行改造
import functools
showString = functools.partial(showData, type=1)
showList = functools.partial(showData, type = 2)
showDict = functools.partial(showData, type = 3)
# 列印字串
showString ("hello functools partial");
# 列印列表
showList ([1,2,3,4,5])
# 列印元組
showList ((1,2,3,4,5))
# 列印集合
showList ({1,2,3,4,5})
# 列印字典
showDict ({"1":"a", "2":"b", "3":"c"})
# * * * * * * * * * * * * * * * * * * * * * *
# 整個世界,清淨了...
# * * * * * * * * * * * * * * * * * * * * * *

```

7. 裝飾器函式處理

裝飾器是在不修改函式本身的程式碼的情況下,對函式的功能進行擴充套件的一個手段

裝飾器,整個名詞是從現實生活中抽象出來的一個概念 所謂裝飾,生活中其實就是不改造原來的物體的情況下給物體增加額外的一些功能的手段,比如一個房子蓋好了~但是不喜歡房子現在的牆壁顏色,不喜歡房子原始地板的顏色,就可以通過裝修的形式,給房子額外增加一些裝飾,讓房子更加的豪華溫馨 此時:房子->裝修->額外的樣式

我們定義一個簡單的函式,用於進行資料的遍歷 ```

# 定義一個函式,可以根據使用者輸入的型別來遍歷資料
def showData(data, *, type=1):
    if type == 1:     #列印字串
        print(data)
    elif type ==2:   # 遍歷列表、元組、集合
        for x in data:
            print(x)
    elif type == 3: # 遍歷字典
        for k, v in data.items():
            print(k, v)

此時,我們想要給這個函式增加額外的功能,在函式執行之前和函式執行後增加額外的日誌的記錄,記錄函式執行的過程,大致功能如下

print("遍歷函式開始執行")
showData("hello my name is showData")
print("遍歷函式執行完成")

``` 這樣的程式碼也是能滿足我們的需要的,但是這個函式的呼叫如果可能出現在很多地方呢?是不是就需要在每次呼叫的時候都要在函式的前後寫這樣的程式碼呢?肯定不太現實

我們通過如下的方式來定義一個函式,包裝我們的showData()函式 ```

# 定義一個包裝函式
def logging(func):
    def wrapper(*args, **kw):
        print("遍歷函式開始執行----")
        res = func(*args, **kw)
        print("遍歷函式執行完成----")
        return res;
    return wrapper
# 在我們原來的函式前面新增一個註解
@logging
def showData(data, *, type=1):
    if type == 1:     #列印字串
        print(data)
    elif type ==2:   # 遍歷列表、元組、集合
        for x in data:
            print(x)
    elif type == 3: # 遍歷字典
        for k, v in data.items():
            print(k, v)

# 執行函式,我們會發現在函式執行時,出現了額外的新增的功能。
showData("my name is jerry!")
# 執行結果
~ 遍歷函式開始執行----
~ my name is jerry!
~ 遍歷函式執行完成----

```

裝飾器函式執行的全過程解析 一、定義過程 1.首先定義好了一個我們的功能處理函式showData(data, * , type = 1) 2.然後定了一個功能擴充套件函式logging(func),可以接受一個函式作為引數 3.使用python的語法@符號,給功能處理函式增加一個標記,將@logging 新增到功能處理函式的前面 二、執行過程 1.直接呼叫執行showData("my name is jerry!")

2.python檢查到函式頂部宣告瞭@logging,將當前函式作為引數傳遞給 logging()函式,就是首先執行logging(showData)

3.功能處理函式的引數"my name is jerry",傳遞給功能擴充套件函式的閉包函式wrapper(*args, **kw)

4.在閉包函式wrapper中,可以通過執行func(*args, **kw)來執行我們的> 功能處理函式showData(),這樣就可以在執行func(*args,**kw)之前和之後新增我們自己需要擴充套件的功能 [備註:函式中的引數,不論傳遞什麼引數,都可以通過(*args, **kw)來接收,請參考函式引數部分內容]

5.執行過程如下圖所示: 裝飾器函式執行過程圖解

相關文章