天天看點

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.執行過程如下圖所示:
19. 再說函數~那些不得不知道的事兒

裝飾器函數執行過程圖解