《Python 基礎篇》五:函式

ACatSmiling發表於2024-09-29

Author: ACatSmiling

Since: 2024-09-27

函式簡介

函式:也是一個物件,物件是記憶體中專門用來儲存資料的一塊區域。函式可以用來儲存一些可執行的程式碼,並且可以在需要時,對這些語句進行多次的呼叫。

建立函式:

def 函式名([形參1, 形參2, ... 形參n]):
    程式碼塊
  • 函式名必須要符號識別符號的規範(可以包含字母、數字、下劃線,但是不能以數字開頭)。
  • 函式中儲存的程式碼不會立即執行,需要呼叫函式程式碼才會執行。

呼叫函式:

函式名()
  • 定義函式一般都是要實現某種功能的。

示例:

# 定義一個函式
def fn():
    print('這是我的第一個函式!')
    print('hello')
    print('今天天氣真不錯!')


# 列印 fn
print(fn)  # <function fn at 0x00000175FD0261F0>
print(type(fn))  # <class 'function'>

# fn 是函式物件,fn() 呼叫函式
# print 是函式物件,print() 呼叫函式
fn()


# 定義一個函式,可以用來求任意兩個數的和
def sum():
    a = 123
    b = 456
    print(a + b)


sum()  # 579


# 定義函式時指定形參
def fn2(a, b):
    print(a, "+", b, "=", a + b)


# 呼叫函式時,來傳遞實參
fn2(10, 20)  # 10 + 20 = 30
fn2(123, 456)  # 123 + 456 = 579

函式的引數

在定義函式時,可以在函式名後的()中定義數量不等的形參,多個形參之間使用,隔開。

  • 形參(形式引數):定義形參就相當於在函式內部宣告瞭變數,但是並不賦值。
  • 實參(實際引數):呼叫函式時,傳入的真正的引數值。
  • 如果函式定義時,指定了形參,那麼在呼叫函式時也必須傳遞實參, 實參將會賦值給對應的形參,簡單來說,有幾個形參就得傳幾個實參。
# 求任意三個數的乘積
def mul(a, b, c):
    print(a * b * c)


mul(1, 2, 3)  # 6


# 根據不同的使用者名稱顯示不同的歡迎資訊
def welcome(username):
    print('歡迎 ', username, ' 光臨')


welcome('孫悟空')  # 歡迎 孫悟空 光臨

引數的傳遞方式

函式的傳參方式有多種:

  1. 預設值引數:定義形參時,可以為形參指定預設值。指定了預設值以後,如果使用者傳遞了引數,則預設值沒有任何作用;如果使用者沒有傳遞引數,則預設值就會生效。
  2. 位置引數:即將對應位置的實參複製給對應位置的形參
  3. 關鍵字引數:可以不按照形參定義的順序去傳遞,而直接根據引數名去傳遞引數
    • 位置引數和關鍵字引數可以混合使用,混合使用時,必須將位置引數寫到前面。
  4. 函式在呼叫時,解析器不會檢查實參的型別,實參可以傳遞任意型別的物件
  5. 在函式中對形參進行重新賦值,不會影響其他的變數。
  6. 如果形參指向的是一個物件,當我們透過形參去修改物件時,會影響到所有指向該物件的變數。

示例:

# 定義函式,併為形參指定預設值
def fn(a=5, b=10, c=20):
    print('a =', a)
    print('b =', b)
    print('c =', c)


fn(1, 2, 3)
fn(1, 2)
fn()

# 位置引數,根據位置傳遞對應的實參
fn(1, 2, 3)

# 關鍵字引數,根據形參名傳遞對應的實參
fn(b=1, c=2, a=3)
print('hello', end='')

# 位置引數和關鍵字引數混合使用,位置引數必須在關鍵字引數前面
fn(1, c=30)


def fn2(a):
    print('a =', a)


# 函式在呼叫時,解析器不會檢查實參的型別
# 實參可以傳遞任意型別的物件
b = 123
fn2(b)  # a = 123
b = True
fn2(b)  # a = True
b = 'hello'
fn2(b)  # a = hello
b = None
fn2(b)  # a = None
b = [1, 2, 3]
fn2(b)  # a = [1, 2, 3]
fn2(fn)  # a = <function fn at 0x0000025AB8B561F0>


# 型別不檢查的缺陷,在傳參時需要額外注意
def fn3(a, b):
    print(a + b)


# fn3(123, "456")  # TypeError: unsupported operand type(s) for +: 'int' and 'str'

# 在函式中對形參進行重新賦值,不會影響其他的變數
def fn4(a):
    a = 20
    print('a =', a, id(a))  # a = 20 140708519029120


c = 10
fn4(c)
print(c)  # 10


# 如果形參指向的是一個物件,當我們透過形參去修改物件時,會影響到所有指向該物件的變數
def fn5(a):
    # a 是一個列表,嘗試修改列表中的元素
    a[0] = 30
    print('a =', a, id(a))  # a = [30, 2, 3] 2056358531968


c = [1, 2, 3]
fn5(c)
print('c =', c, id(c))  # c = [30, 2, 3] 2056358531968

# 透過淺複製,或者切片,實現不修改 c 本身
fn4(c.copy())
fn4(c[:])

Java 的引數傳遞機制

在 Java 中,基本資料型別(如 int、double、char 等)是按值傳遞的,即傳遞的是變數的值的副本。對於引用資料型別(如物件、陣列等),實際上傳遞的是物件引用的副本,但從效果上看,可以透過這個引用副本去操作物件本身。

Python 的引數傳遞機制

Python 的引數傳遞通常被認為是 "物件引用傳遞"。不可變物件(如整數、字串、元組等)在函式內部的行為看起來像是值傳遞,因為不能直接修改不可變物件,一旦嘗試修改,實際上是建立了新的物件。而可變物件(如列表、字典、集合等)在函式內部的修改會影響到外部的物件,因為傳遞的是物件的引用。

二者對比

基本資料型別的傳遞:

  • Java:對於基本資料型別(如 int、double、char 等),Java 是嚴格的值傳遞。這意味著當把一個基本資料型別的變數作為引數傳遞給方法時,會將該變數的值複製一份傳遞過去,在方法內部對這個引數的修改不會影響到原始變數的值。

     public class JavaValuePassing {
         public static void main(String[] args) {
             int num = 10;
             changeNumber(num);
             System.out.println(num); // 輸出仍然是 10
         }
    
         public static void changeNumber(int n) {
             n = 20;
         }
     }
    
  • Python:Python 中沒有嚴格意義上的基本資料型別和引用資料型別的區分。但對於不可變的基本資料型別類似物(如整數、字串等),在函式呼叫時的行為有點類似於值傳遞。然而,實際上是物件引用的傳遞,只是因為不可變物件一旦被修改就會建立新的物件,所以看起來像是值傳遞。

     def change_number(num):
         num = 20
    
     a = 10
     change_number(a)
     print(a) # 輸出仍然是 10
    

引用資料型別的傳遞:

  • Java:對於引用資料型別(如物件、陣列等),Java 傳遞的是物件引用的副本。這意味著在方法內部可以透過這個引用副本去操作物件本身,修改物件的屬性會影響到外部的物件。但是,如果將這個引用重新指向一個新的物件,不會影響到原始的引用。

     class Person {
         String name;
    
         public Person(String name) {
             this.name = name;
         }
    
         public String getName() {
             return name;
         }
    
         public void setName(String name) {
             this.name = name;
         }
     }
    
     public class JavaValuePassing {
         public static void main(String[] args) {
             Person person = new Person("Alice");
             changePerson(person);
             System.out.println(person.getName()); // 可能輸出修改後的名字
         }
    
         public static void changePerson(Person p) {
             p.setName("Bob");
             // 如果重新建立一個新的物件並賦值給引數 p
             // p = new Person("Charlie"); 這樣不會影響到外部的 person 引用
         }
     }
    
  • Python:對於可變的引用資料型別類似物(如列表、字典、集合等),傳遞的是物件引用。在函式內部對這個物件的修改會影響到外部的物件。對於不可變的引用資料型別類似物(如元組),雖然傳遞的也是物件引用,但由於元組本身不可變,在函式內部無法修改其內容。

     def change_list(lst):
         lst.append(20)
    
     my_list = [1, 2, 3]
     change_list(my_list)
     print(my_list) # 輸出為 [1, 2, 3, 20]
    

小結:

  • Java:
    • Java 的值傳遞機制相對較為明確和嚴格,基本資料型別和引用資料型別的傳遞方式有明顯的區分,並且對於引用資料型別的操作有一定的限制,不會因為意外的重新賦值而影響到外部的引用。
    • 開發者在使用時可以比較清楚地知道引數傳遞的效果,不容易出現意外的錯誤。
  • Python:
    • Python 的引數傳遞機制更加靈活,但也可能因為這種靈活性而導致一些難以察覺的錯誤。開發者需要根據物件的可變性來理解引數傳遞的行為,並且要注意在函式內部對可變物件的修改可能會影響到外部的物件。
    • Python 的這種特性在某些情況下可以使程式碼更加簡潔和高效,但也需要開發者更加小心地處理引數傳遞和物件的修改。

不定長的引數

在定義函式時,可以在形參前邊加上一個*,這樣這個形參將會獲取到所有的實參,並將所有的實參儲存到一個元組中。

  • *的形參只能有一個。
  • *的引數,可以和其他引數配合使用。
  • 可變引數不是必須寫在最後,但是注意,帶*的引數後的所有引數,必須以關鍵字引數的形式傳遞,否則報錯。
  • 如果在形參的開頭直接寫一個*,則要求所有的引數必須以關鍵字引數的形式傳遞。
  • *形參只能接收位置引數,而不能接收關鍵字引數。
  • **形參可以接收其他的關鍵字引數,它會將這些引數統一儲存到一個字典中。字典的 key 就是引數的名字,字典的 value 就是引數的值。
  • **形參只能有一個,並且必須寫在所有引數的最後。
  • 傳遞實參時,也可以在序列型別的引數前新增*,這樣會自動將序列中的元素依次作為引數傳遞給函式,但要求序列中元素的個數必須和形參的個數一致。
  • 如果是字典,透過**來進行解包操作。
# 定義一個函式,可以求任意個數字的和
def sum(*nums):
    # 定義一個變數,來儲存結果
    result = 0
    # 遍歷元組,並將元組中的數進行累加
    for n in nums:
        result += n
    print(result)


sum(10, 20, 30, 40)  # 100
sum(10, 20, 30, 40, 50, 60, 70)  # 280


# *a 會接受所有的位置實參,並且會將這些實參統一儲存到一個元組中(裝包)
def fn(*a):
    print("a =", a, type(a))


fn(1, 2, 3)  # a = (1, 2, 3) <class 'tuple'>
fn(1, 2, 3, 4, 5)  # a = (1, 2, 3, 4, 5) <class 'tuple'>


# 帶星號的形參只能有一個
# 帶星號的引數,可以和其他引數配合使用
# 下面的函式,第一個引數給a,第二個引數給b,剩下的都儲存到c的元組中
def fn2(a, b, *c):
    print('a =', a)
    print('b =', b)
    print('c =', c)


fn2(1, 2, 3, 4, 5)


# 可變引數不是必須寫在最後,但是注意,帶 * 的引數後的所有引數,必須以關鍵字引數的形式傳遞
# 下面的函式,第一個引數給 a,剩下的位置引數給 b 的元組,c 必須使用關鍵字引數
def fn3(a, *b, c):
    print('a =', a)
    print('b =', b)
    print('c =', c)


fn3(1, 2, 3, 4, c=5)


# 下面的函式,所有的位置引數都給 a,b 和 c 必須使用關鍵字引數
def fn4(*a, b, c):
    print('a =', a)
    print('b =', b)
    print('c =', c)


fn4(1, 2, 3, b=4, c=5)


# 如果在形參的開頭直接寫一個 *,則要求我們的所有的引數必須以關鍵字引數的形式傳遞
def fn5(*, a, b, c):
    print('a =', a)
    print('b =', b)
    print('c =', c)


fn5(a=3, b=4, c=5)


# * 形參只能接收位置引數,而不能接收關鍵字引數
# def fn3(*a) :
#     print('a =',a)

# ** 形參可以接收其他的關鍵字引數,它會將這些引數統一儲存到一個字典中
#   字典的 key 就是引數的名字,字典的 value 就是引數的值
# ** 形參只能有一個,並且必須寫在所有引數的最後
def fn6(b, c, **a):
    print('a =', a, type(a))
    print('b =', b)
    print('c =', c)


fn6(b=1, c=2, h=3, e=10, f=20)
fn6(6, 7, g=1, d=2, h=3, e=10, f=20)

print('##########################################################')


# 引數的解包(拆包)

def fn7(a, b, c):
    print('a =', a)
    print('b =', b)
    print('c =', c)


# 傳遞實參時,也可以在序列型別的引數前新增星號,這樣他會自動將序列中的元素依次作為引數傳遞給函式
# 這裡要求序列中元素的個數必須和形參的個數的一致
t = (10, 20, 30)
fn7(*t)

# 透過 **來對一個字典進行解包操作
d = {'a': 100, 'b': 200, 'c': 300}
fn7(**d)

函式的返回值

函式的返回值就是函式執行以後返回的結果,可以透過return來指定函式的返回值。

  • return 後邊可以跟任意的物件,甚至可以是一個函式。return 後邊跟什麼值,函式就會返回什麼值。
  • 如果僅僅寫一個 return 或者不寫 return,則相當於 return None。
  • 在函式中,return 後的程式碼都不會執行,return 一旦執行函式自動結束。

示例:

# return 後邊跟什麼值,函式就會返回什麼值
# return 後邊可以跟任意的物件,返回值甚至可以是一個函式
def fn():
    # return 'Hello'
    # return [1, 2, 3]
    # return {'k': 'v'}
    def fn1():
        print('hello')

    return fn1  # 返回值也可以是一個函式


r = fn()  # 這個函式的執行結果就是它的返回值
print(r)  # <function fn.<locals>.fn1 at 0x000001F3430BCF70>
r()  # hello


# 如果僅僅寫一個 return 或者不寫 return,則相當於 return None
# 在函式中,return 後的程式碼都不會執行,return 一旦執行函式自動結束
def fn2():
    a = 10
    return
    print('abc')  # 不會執行


r = fn2()
print(r)  # None


def fn3():
    for i in range(5):
        if i == 3:
            # break 用來退出當前迴圈
            # continue 用來跳過當次迴圈
            return  # return 用來結束函式
        print(i)
    print('迴圈執行完畢!')


fn3()


def fn4():
    return 10


# fn4 和 fn4()的區別
print(fn4)  # fn4 是函式物件,列印 fn4 實際是在列印函式物件:<function fn5 at 0x00000229B3CFD670>
print(fn4())  # fn4() 是在呼叫函式,列印 fn4() 實際上是在列印 fn4() 函式的返回值:10

文件字串

文件字串:在定義函式時,可以在函式內部編寫文件字串,文件字串就是函式的說明。文件字串非常簡單,其實直接在函式的第一行寫一個字串就是文件字串,當我們編寫了文件字串時,就可以透過help()函式來檢視函式的說明。

示例:

# help() 是 Python 中的內建函式
# 透過 help() 函式可以查詢 Python 中的函式的用法
# 語法:help(函式物件)
help(print)  # 獲取 print() 函式的使用說明


# 文件字串(doc str)
def fn(a: int, b: bool, c: str = 'hello') -> int: # 函式引數後跟著型別,返回值是一個 int
    """
    這是一個文件字串的示例

    函式的作用:。。。。。。
    函式的引數:
        a,作用,型別,預設值。。。
        b,作用,型別,預設值。。。
        c,作用,型別,預設值。。。
    """
    return 10


help(fn)

作用域

作用域(scope):指的是變數生效的區域。在 Python 中一共有兩種作用域:

  1. 全域性作用域:全域性作用域在程式執行時建立,在程式執行結束時銷燬。
    • 所有函式以外的區域都是全域性作用域。
    • 在全域性作用域中定義的變數,都屬於全域性變數,全域性變數可以在程式的任意位置被訪問。
  2. 函式作用域:函式作用域在函式呼叫時建立,在呼叫結束時銷燬。
    • 函式每呼叫一次就會產生一個新的函式作用域。
    • 在函式作用域中定義的變數,都是區域性變數,它只能在函式內部被訪問。
    • 如果希望在函式內部修改全域性變數,則需要使用 global 關鍵字,來宣告變數。

變數的查詢過程:當我們使用變數時,會優先在當前作用域中尋找該變數,如果有則使用;如果沒有則繼續去上一級作用域中尋找,如果有則使用;如果依然沒有則繼續去上一級作用域中尋找,以此類推,直到找到全域性作用域,依然沒有找到,則會丟擲異常 "NameError: name 'a' is not defined"。

示例:

b = 20  # 全域性變數


def fn():
    a = 10  # a 定義在了函式內部,所以他的作用域就是函式內部,函式外部無法訪問
    print('函式內部:', 'a =', a)
    print('函式內部:', 'b =', b)


fn()

# print('函式外部:', 'a =', a)  # NameError: name 'a' is not defined
print('函式外部:', 'b =', b)



def fn1():
    def fn2():
        print('fn3中:', 'a =', a)

    fn2()


# fn1() # fn1 中的巢狀函式 fn2,找不到 a,報錯 NameError: name 'a' is not defined

a = 20


def fn3():
    # a = 10 # 在函式中為變數賦值時,預設都是為區域性變數賦值
    # 如果希望在函式內部修改全域性變數,則需要使用 global 關鍵字,來宣告變數
    global a  # 宣告在函式內部使用的 a 是全域性變數,此時再去修改 a 時,就是在修改全域性的 a
    a = 10  # 修改全域性變數
    print('函式內部:', 'a =', a)


fn3()
print('函式外部:', 'a =', a)  # 函式外部: a = 10

名稱空間

名稱空間(namespace):指的是變數儲存的位置,每一個變數都需要儲存到指定的名稱空間當中。

  • 每一個作用域都會有一個它對應的名稱空間。
  • 全域性名稱空間,用來儲存全域性變數;函式名稱空間,用來儲存函式中的變數。
  • 名稱空間實際上就是一個字典,是一個專門用來儲存變數的字典
  • 使用locals()函式,可以獲取當前作用域的名稱空間,其返回值是一個字典。
    • 如果在全域性作用域中呼叫 locals() 則獲取全域性名稱空間,如果在函式作用域中呼叫 locals() 則獲取函式名稱空間。
    • 使用globals()函式,可以用來在任意位置獲取全域性名稱空間。

示例:

scope = locals()  # 當前名稱空間
print(type(scope))  # <class 'dict'>
# 下面兩個列印效果相同
a = 20
print(a)  # 20
print(scope['a'])  # 20


# 向 scope 中新增一個 key-value
# scope['c'] = 1000  # 向字典中新增 key-value 就相當於在全域性中建立了一個變數(一般不建議這麼做)
# print(c)  # 1000


def fn():
    a = 10
    scope = locals()  # 在函式內部呼叫 locals() 會獲取到函式的名稱空間
    print(type(scope))  # <class 'dict'>

    # scope['b'] = 90  # 可以透過 scope 來操作函式的名稱空間,但是也是不建議這麼做
    # print(b)

    # globals() 函式可以用來在任意位置獲取全域性名稱空間
    # 函式外面無法獲得函式的名稱空間
    global_scope = globals()
    print(global_scope['a'])  # 20
    # global_scope['a'] = 30 # 不建議這麼做
    # print(global_scope['a'])  # 30


fn()

遞迴

遞迴:遞迴是解決問題的一種方式,它和迴圈很像,它的整體思想是,將一個大問題分解為一個個的小問題,直到問題無法分解時,再去解決問題。

遞迴式函式的兩個要件:

  1. 基線條件:問題可以被分解為的最小問題,當滿足基線條件時,遞迴就不在執行了。
  2. 遞迴條件:將問題繼續分解的條件。

示例:

# 10 的階乘
n = 10
for i in range(1, 10):
    n *= i

print(n)


# 建立一個函式,可以用來求任意數的階乘
def factorial(n):
    '''
        該函式用來求任意數的階乘

        引數:
            n 要求階乘的數字
    '''

    # 建立一個變數,來儲存結果
    result = n

    for i in range(1, n):
        result *= i

    return result


print(factorial(10))


# 遞迴簡單理解就是自己去引用自己!
# 遞迴式函式,在函式中自己呼叫自己!

# 下面這個是無窮遞迴,如果這個函式被呼叫,程式的記憶體會溢位,效果類似於死迴圈
# def fn():
#     fn()
# fn()


# 遞迴和迴圈類似,基本是可以互相代替的,
#   迴圈編寫起來比較容易,閱讀起來稍難
#   遞迴編寫起來難,但是方便閱讀

def factorial(n):
    # 基線條件:判斷 n 是否為 1,如果為 1,則此時不能再繼續遞迴
    if n == 1:
        # 1 的階乘就是 1,直接返回 1
        return 1
    # 遞迴條件
    return n * factorial((n - 1))


print(factorial(10))


# 建立一個函式 power,來為任意數字做冪運算 n ** i
def power(n, i):
    '''
        power() 用來為任意的數字做冪運算

        引數:
            n 要做冪運算的數字
            i 做冪運算的次數
    '''
    # 基線條件
    if i == 1:
        # 求1次冪
        return n
    # 遞迴條件
    return n * power(n, i - 1)


print(pow(3, 4))


# 建立一個函式,用來檢查一個任意的字串是否是迴文字串,如果是返回 True,否則返回 False
# 迴文字串,字串從前往後念和從後往前念是一樣的
# abcba
# abcdefgfedcba
# 先檢查第一個字元和最後一個字元是否一致,如果不一致則不是迴文字串
#     如果一致,則看剩餘的部分是否是迴文字串
# 檢查 abcdefgfedcba 是不是迴文
# 檢查 bcdefgfedcb 是不是迴文
# 檢查 cdefgfedc 是不是迴文
# 檢查 defgfed 是不是迴文
# 檢查 efgfe 是不是迴文
# 檢查 fgf 是不是迴文
# 檢查 g 是不是迴文
def hui_wen(s):
    '''
        該函式用來檢查指定的字串是否迴文字串,如果是返回 True,否則返回 False

        引數:
            s:就是要檢查的字串
    '''
    # 基線條件
    if len(s) < 2:
        # 字串的長度小於 2,則字串一定是迴文
        return True
    elif s[0] != s[-1]:
        # 第一個字元和最後一個字元不相等,不是迴文字串
        return False
        # 遞迴條件
    return hui_wen(s[1:-1])


# def hui_wen(s):
#     '''
#         該函式用來檢查指定的字串是否迴文字串,如果是返回 True,否則返回 False

#         引數:
#             s:就是要檢查的字串
#     '''
#     # 基線條件
#     if len(s) < 2 :
#         # 字串的長度小於 2,則字串一定是迴文
#         return True
#     # 遞迴條件
#     return s[0] == s[-1] and hui_wen(s[1:-1])

print(hui_wen('abba'))
print(hui_wen('abcdefgfedcba'))

高階函式

高階函式:接收函式作為引數,或者將函式作為返回值的函式是高階函式。當我們使用一個函式作為引數時,實際上是將指定的程式碼傳遞進了目標函式。

在 Python 中,函式是一等物件。一等物件一般都會具有如下特點:

  • 物件是在執行時建立的。
  • 能賦值給變數或作為資料結構中的元素。
  • 能作為引數傳遞。
  • 能作為返回值返回。

高階函式至少要符合以下兩個特點中的一個:

  • 能接收一個或多個函式作為引數。
  • 能將函式作為返回值返回。

示例:

# 定義一個函式,功能:可以將指定列表中的所有的偶數,儲存到一個新的列表中返回

# 待提取列表
l = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]


# 常規寫法

def fn(lst):
    '''
        fn() 函式可以將指定列表中的所有偶數提取出來,並儲存到一個新列表中返回

        引數:
            lst:指定的要篩選的列表
    '''
    new_list = []
    for n in lst:
        if n % 2 == 0:
            new_list.append(n)
    return new_list


print(fn(l))  # [2, 4, 6, 8, 10]


# 高階函式

# 功能 1:定義一個函式,用來檢查一個任意的數字是否是偶數
def fn1(i):
    if i % 2 == 0:
        return True

    return False


# 功能 2:定義一個函式,用來檢查指定的數字是否大於 5
def fn2(i):
    if i > 5:
        return True
    return False


# 功能 2:定義一個函式,用來檢查一個任意的數字是否能被 3 整除
def fn3(i):
    return i % 3 == 0


# 多功能的高階函式

def fn(func, lst):
    '''
        fn() 函式可以將指定列表中的資料按指定函式要求提取出來,並儲存到一個新列表中返回

        引數:
            func:指定的提取要求
            lst:指定的要篩選的列表
    '''
    new_list = []
    for n in lst:
        if func(n):
            new_list.append(n)
    return new_list


print(fn(fn1, l))  # 獲取偶數:[2, 4, 6, 8, 10]
print(fn(fn2, l))  # 獲取大於 5 的數:[6, 7, 8, 9, 10]
print(fn(fn3, l))  # 獲取能被 3 整除的數:[3, 6, 9]

# filter() 函式的功能,就如上面自定義的 fn() 函式
# filter() 可以從序列中過濾出符合條件的元素,儲存到一個新的序列中
# 引數:
#  1. 函式,根據該函式來過濾序列(可迭代的結構)
#  2. 需要過濾的序列(可迭代的結構)
# 返回值:
#   過濾後的新序列(可迭代的結構)

iterator = filter(fn1, l)
# for n in iterator:
#     print(n)
print(list(iterator))  # 返回的是一個可迭代的結構,需要轉換成 list 才能直接列印出來資料
print(list(filter(fn2, l)))
print(list(filter(fn3, l)))

匿名函式

匿名函式:將一個或多個函式作為引數來接收。匿名函式一般都是作為引數使用,其他地方一般不會使用。

語法:lambda 引數列表 : 返回值

示例:

# 在前面的示例中,fn1 ~ fn3 是作為引數傳遞進 filter() 函式中
#   而 fn1 ~ fn3 實際上只有一個作用,就是作為 filter() 的引數
#   filter() 呼叫完畢以後,fn1 ~ fn3 就已經沒用
#   這種情況可以用匿名函式簡化
# 匿名函式 lambda 函式表示式(語法糖)
#   lambda 函式表示式專門用來建立一些簡單的函式,他是函式建立的又一種方式
#   語法:lambda 引數列表 : 返回值
#   匿名函式一般都是作為引數使用,其他地方一般不會使用

# 待提取列表
l = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]


# 常規寫法
def fn5(a, b):
    return a + b
print(fn5(10, 20))  # 常規寫法

print((lambda a, b: a + b)(10, 20))  # lambad 表示式寫法

fn6 = lambda a, b: a + b  # 也可以將匿名函式賦值給一個變數,一般不會這麼做
print(fn6(10, 20))

# filter() 函式中可以很方便的使用 lambda 表示式
#   此時,lambda 表示式只會使用一次,使用完後記憶體中自動回收
r = filter(lambda i: i % 2 == 0, l) # lambda i: i % 2 == 0 是匿名函式
print(list(r))  # [2, 4, 6, 8, 10]
print(list(filter(lambda i: i > 5, l)))  # [6, 7, 8, 9, 10],lambda i: i > 5 是匿名函式

# map() 函式可以對可迭代物件中的所有元素做指定的操作,然後將其新增到一個新的物件中返回

print(list(map(lambda i: i ** 2, l)))  # 對列表中的每一個元素求平方,[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

# sort() 函式用來對列表中的元素進行排序:
#   sotr() 只能排序列表
#   預設是直接比較列表中的元素的大小
# 在 sort() 可以接收一個關鍵字引數 key:
#   key 需要一個函式作為引數,當設定了函式作為引數
#   每次都會以列表中的一個元素作為引數來呼叫該函式
#   並且使用函式的返回值來比較元素的大小
l = ['bb', 'aaaa', 'c', 'ddddddddd', 'fff']
l.sort()
print(l)  # 預設比較:['aaaa', 'bb', 'c', 'ddddddddd', 'fff']

l = ['bb', 'aaaa', 'c', 'ddddddddd', 'fff']
l.sort(key=len)
print(l)  # 按長度比較:['c', 'bb', 'fff', 'aaaa', 'ddddddddd']

l = [2, 5, '1', 3, '6', '4']
l.sort(key=int)
print(l)  # 把每一個元素轉換成整形後再比較:['1', 2, 3, '4', 5, '6']

l = [2, 5, '1', 3, '6', '4']
l.sort(key=str)
print(l)  # 把每一個元素轉換成字串後再比較:['1', 2, 3, '4', 5, '6']

# sorted() 函式和 sort() 的用法基本一致,但是 sorted() 可以對任意的序列進行排序
#   並且使用 sorted() 排序不會影響原來的物件,而是返回一個新物件

l = [2, 5, '1', 3, '6', '4']  # 排序列表
print('排序前:', l)  # 排序前:[2, 5, '1', 3, '6', '4']
print('排序中:', sorted(l, key=int))  # 排序中:['1', 2, 3, '4', 5, '6']
print('排序後:', l)  # 排序後:[2, 5, '1', 3, '6', '4']

l = '123765816742634781'  # 排序字串
print('排序前:', l)  # 排序前:123765816742634781
print('排序中:', sorted(l, key=int))  # 排序中:['1', '1', '1', '2', '2', '3', '3', '4', '4', '5', '6', '6', '6', '7', '7', '7', '8', '8']
print('排序後:', l)  # 排序後:123765816742634781

閉包

閉包:是指在一個函式內部定義另一個函式,並且內部函式可以訪問外部函式的區域性變數。即使外部函式已經執行完畢,內部函式仍然可以訪問外部函式的區域性變數。

形成閉包的要件:

  1. 函式巢狀。
  2. 將內部函式作為返回值返回。
  3. 內部函式必須要使用到外部函式的變數。

示例:

# 將函式作為返回值返回,也是一種高階函式
# 這種高階函式我們也稱為叫做閉包,透過閉包可以建立一些只有當前函式能訪問的變數
#   可以將一些私有的資料藏到閉包中

def fn():
    a = 10

    # 函式內部再定義一個函式
    def inner():
        print('我是fn2', a)

    # 將內部函式 inner 作為返回值返回
    return inner


# r 是一個函式,是呼叫 fn() 後返回的函式
# 而且這個函式是在 fn() 內部定義,並不是全域性函式
# 所以這個函式總是能訪問到 fn() 函式內的變數
r = fn()
print(r)  # <function fn.<locals>.inner at 0x000001CEC1142430>
r()  # 我是fn2 10

# 求多個數的平均值
nums = [50, 30, 20, 10, 77]

# 常規寫法:sum() 用來求一個列表中所有元素的和
print(sum(nums) / len(nums))  # 37.4


# 如果 nums 中的資料是變化的,可以使用閉包
def make_averager():
    # 建立一個列表,用來儲存數值
    nums = []

    # 建立一個函式,用來計算平均值
    def averager(n):
        # 將n新增到列表中
        nums.append(n)
        # 求平均值
        return sum(nums) / len(nums)

    return averager


# 函式返回的是 make_averager() 中定義的 averager() 函式
# 並建立了一個 nums 列表,這個列表外界無法訪問,只有 averager 物件可以訪問
averager = make_averager()

print(averager(10))  # 10/1=10
print(averager(20))  # (10+20)/2=15
print(averager(30))  # (10+20+30)/3=20
print(averager(40))  # (10+20+30+40)/4=25

裝飾器

# 建立幾個函式

def add(a, b):
    '''
        求任意兩個數的和
    '''
    r = a + b
    return r


def mul(a, b):
    '''
        求任意兩個數的積
    '''
    r = a * b
    return r


print(add(123, 456))  # 579
print(mul(10, 20))  # 200


# 現在,希望函式可以在計算前,列印開始計算,計算結束後列印計算完畢
#  我們可以直接透過修改函式中的程式碼來完成這個需求,但是會產生以下一些問題
#   ① 如果要修改的函式過多,修改起來會比較麻煩
#   ② 並且不方便後期的維護
#   ③ 並且這樣做會違反開閉原則(OCP)
#           程式的設計,要求開發對程式的擴充套件,要關閉對程式的修改


# 我們希望在不修改原函式的情況下,來對函式進行擴充套件
def fn():
    print('我是fn函式....')


# 只需要根據現有的函式,來建立一個新的函式
def fn2():
    print('函式開始執行~~~')
    fn()
    print('函式執行結束~~~')


fn2()


# 建立新函式,擴充套件 add()
def new_add(a, b):
    print('計算開始~~~')
    r = add(a, b)
    print('計算結束~~~')
    return r


r = new_add(111, 222)
print(r)

print('##############################################################')


# 上邊的方式,已經可以在不修改原始碼的情況下對函式進行擴充套件了
#   但是,這種方式要求我們每擴充套件一個函式就要手動建立一個新的函式,實在是太麻煩了
#   為了解決這個問題,我們建立一個函式,讓這個函式可以自動的幫助我們生產函式

def begin_end(old):
    '''
        用來對其他函式進行擴充套件,使其他函式可以在執行前列印開始執行,執行後列印執行結束

        引數:
            old 要擴充套件的函式物件
    '''

    # 建立一個新函式,引數的個數是不確定的,使用*和**
    def new_function(*args, **kwargs):
        print('開始執行~~~~')
        # 呼叫被擴充套件的函式
        result = old(*args, **kwargs)
        print('執行結束~~~~')
        # 返回函式的執行結果
        return result

    # 返回新函式
    return new_function


f = begin_end(fn)  # 包裝 fn()
f2 = begin_end(add)  # 包裝 add()
f3 = begin_end(mul)  # 包裝 mul()

r = f()
print(r)
r = f2(123, 456)
print(r)
r = f3(123, 456)
print(r)

print('##############################################################')


# 像 begin_end() 這種函式我們就稱它為裝飾器
#   透過裝飾器,可以在不修改原來函式的情況下來對函式進行擴充套件
#   在開發中,我們都是透過裝飾器來擴充套件函式的功能的
# 在定義函式時,可以透過 @裝飾器,來使用指定的裝飾器,來裝飾當前的函式
#   可以同時為一個函式指定多個裝飾器,這樣函式將會安裝從內向外的順序被裝飾

def fn3(old):
    '''
        用來對其他函式進行擴充套件,使其他函式可以在執行前列印開始執行,執行後列印執行結束

        引數:
            old 要擴充套件的函式物件
    '''

    # 建立一個新函式
    # *args:old 函式中的位置引數(形如:'a, b'),全都存放其中
    # **kwargs:old 函式中的字典引數(形如:'a=x, b=y'),全部存放其中
    def new_function(*args, **kwargs):
        print('fn3裝飾~開始執行~~~~')
        # 呼叫被擴充套件的函式
        result = old(*args, **kwargs)
        print('fn3裝飾~執行結束~~~~')
        # 返回函式的執行結果
        return result

    # 返回新函式
    return new_function


@fn3  # 第一層:fn3 裝飾
@begin_end  # :第二層:begin_end 裝飾
def say_hello():
    print('大家好~~~')


say_hello()

原文連結

https://github.com/ACatSmiling/zero-to-zero/blob/main/PythonLanguage/python.md

相關文章