Py 立得 — sorted function 中的 key=lambda…是什麼操作🤔?

深入解析 lambda function 的語法與運用情境

SleeperShark
ccClub
16 min readJul 17, 2024

--

從這篇文章中,你將會學到:

  • lambda 的使用方法與情境
  • 將 function 作為 parameter 傳入並執行
  • 透過 lambda function 來建立 sorted function 的 key parameter 的變數
  • 回家作業:練習設計出符合條件的 lambda function

當初第一次看到 lambda 這個用法時,是在一個 sorted function 的情境中,看的是一頭霧水:

basket_a = {"name": "a", "bananas": 25, "apples": 66, "mango": 16}
basket_b = {"name": "b", "bananas": 13, "apples": 74, "mango": 24}
basket_c = {"name": "c", "bananas": 35, "apples": 12, "mango": 30}

baskets = [
basket_a,
basket_b,
basket_c
]

# What does lambda mean anyway??? :(
sorted_by_banana = sorted(baskets, key=lambda basket: basket["bananas"])

for basket in sorted_by_banana:
print(basket)
# {'name': 'b', 'bananas': 13, 'apples': 74, 'mango': 24}
# {'name': 'a', 'bananas': 25, 'apples': 66, 'mango': 16}
# {'name': 'c', 'bananas': 35, 'apples': 12, 'mango': 30}

如果你跟我當時的我一樣,「完全不知道這裡發生什麼事 😖」,或者是隱隱約約知道 lambda 的用法、卻不太理解這裡的機制的話,就跟著這一篇文章,一次搞懂 lambda 與 function 參數的眉眉角角吧!

關鍵一:lambda 是 function 的偷懶寫法 😝

w3schools 的 lambda 說明文件 描述中,lambda function 實際上是一個 anonymous function 匿名函式

等一下,函式 function 我知道是什麼,但「匿名函式」又是什麼意思 ?

一言以蔽之,匿名函式是一種偷懶寫法,讓你不用大費周章地從 def 開始設計函式名稱、參數、code block 與最後的回傳值,而是只關注以下兩個關鍵:

  • 傳入參數
  • 回傳值

我們看個簡單的例子🌰:

def sum_up(a, b):
return a+b

print(sum_up(10, 20)) # 30

上面的程式碼中,我們定義了一個簡單的 function sum_up ,回傳輸入的兩個參數的和。

如果用 lambda 來改寫上面的函數的話,就會變成這樣:

lambda_sum_up = lambda a, b: a+b
# NOTE: 這裡的 lambda_sum_up 不是 "function 名稱"
# 而是一個存有這個匿名函示記憶體位置的 "變數"!

print(lambda_sum_up(10, 20)) # 30

可以發現,透過lambda 定義的 function,就無需透過 def 語句定義函數名稱,也沒有 function 的 code block。

而在 lambda 關鍵字後,

  • 冒號左邊 a, b的部分代表的是要傳入這個匿名函式的參數
  • 冒號右邊 a+b 的部分則是根據前面參數運算後,要回傳的結果

最後,我們用 lambda_sum_up 這個變數名稱來承接 lambda 建立的 function,並透過呼叫 lambda_sum_up 變數來使用 function。

我們再來看另一個例子🌰
函式:接收三個整數後,回傳其中的最大值整數的平方
然後分別用「正常函式」與「 lambda 函式」來設計

# 如果用正常函數來設計: 
def square_of_the_max(a, b, c):
max_of_nums = max([a, b, c]) # 先取得三者中的最大值
return max_of_nums**2 # 回傳最大的平方

print(square_of_the_max(2, 10, 7)) # 100

# ------------------------------------------

# 如果用 lambda 建構式來設計:
lambda_square_of_the_max = lambda a, b, c: max([a, b, c])**2

print(lambda_square_of_the_max(11, 6, 20)) # 400

# ------------------------------------------

# 你甚至可以直接觸發 lambda 函式,不用再另外把它 assign 到變數上再執行!
result = (lambda a, b, c: max([a, b, c])**2)(1, 2, 3)
print(result) # 9

本質上,lambda 就是建立函式,因此原本的函式特性,都可以在 lambda 中達成。

回到一開始提到的 sum_up的例子🌰
如果我希望函式
sum_up 參數 b 會有預設值 5 的話呢 🤔

# 用 def 來建立函示...
def sum_up(a, b=5):
return a+b

print(sum_up(10)) # 15,因為 b 的預設值為 5
print(sum_up(10, 20)) # 30

# lambda 來建立函示...
lambda_sum_up = lambda a,b=5: a+b

print(lambda_sum_up(10)) # 15
print(lambda_sum_up(10, 20)) # 30

小結一下,所謂的 python lambda 語法:

  1. 快速建立一個結構單純的 function
    (是一種語法糖 syntax sugar)
  2. 冒號左邊:定義要傳入這個 function 的參數
  3. 冒號右邊:定義這個 function 要回傳的結果

還記得我們剛剛說 lambda 是一個 “匿名函示 (anonymous function)” 嗎?如果你對這個說明還有點不明就理的話,執行看看下面這段程式,看看會出現什麼結果吧!

def normal_sum_up(a, b):
return a+b

lambda_sum_up = lambda a, b: a+b

print(normal_sum_up.__name__)
# normal_sum_up

print(lambda_sum_up.__name__)
# 會印出什麼呢 ?

關鍵二:function 也作為參數傳入 😮

在知道 lambda 的用法以後,第二個要認識的概念,就是 function 也能當作參數來使用

過去我們學到,變數 (variables) 的概念就是「儲存資料」,不管是 整數 int、字串 str、布林 bool、浮點數 float、串列 list、字典 dictionary、甚至是自己定義的 class 所初始化的 instance,他們的功能就是將某個資料值儲存,並在未來透過呼叫變數名稱的方式存取。

函式 (function) 則是一組定義一系列操作變數行為的方式,透過 def 定義函示、傳入參數、在 code block 中設計要如何處理參數、並回傳最後的結果。

對很多初學者來說,變數與函式看起來在功能、目的上完全不同,理所當然不會將兩者視為一樣的概念;
不過,可能會出乎你意料的是,對於 Python 來說,

函式,本質上也是一種變數

讓我們用 type function 來觀察一下:

a_int = 1
b_list = [1, 2, 3]
c_dict = {"message": "Show me what you got!"}

def greeting():
print("ccClub the best")

print(type(a_int)) # <class 'int'>
print(type(b_list)) # <class 'list'>
print(type(c_dict)) # <class 'dict'>
print(type(greeting)) # <class 'function'>

從上面的程式碼可以觀察到,greeting 函式是一個型別為 function 的物件變數;既然是變數,那函式當然也可以作為另一個函式的參數來傳入

當我們將函式 A 做為參數,傳入函式 B 的時候,目的是在函式 B 的流程中執行函式 A。

讓我們看個簡單的例子 🌰:

def line_1():
print("Go get it tiger!")

def line_2():
print("I can do this all day.")


def prepare_to_say_the_line(line_func):
print("Testing... Prepare to say the line...")
line_func()
print("Test done.")


prepare_to_say_the_line(line_1)
# Testing... Prepare to say the line...
# Go get it tiger!
# Test done.

prepare_to_say_the_line(line_2)
# Testing... Prepare to say the line...
# I can do this all day.
# Test done.

prepare_to_say_the_line 函式中,執行了傳入的函式參數 line_func,而實際的結果則根據傳入的 line_func 函式 (line_1 , line_2 )而有所不同。

如果你傳入的參數不是函式,則會得到以下錯誤訊息 ⛔:

>> prepare_to_say_the_line("line_1")

Traceback (most recent call last):
File "/home/main.py", line 26, in <module>
prepare_to_say_the_line("line_1")
File "/home/main.py", line 18, in prepare_to_say_the_line
line_func()
TypeError: 'str' object is not callable

Type Error 說明,傳入的參數非「可調用的 callable」— 也就是非 function,不能透過 () 的方式執行。

小結一下,關鍵二的知識點:

  1. 任何函式都是一種型別為 function 的物件變數,跟常見的 str , list 等等一樣。
  2. 函式 A 可以作為另一個函式 B 的參數來傳入,並在函式 B 中執行。

結論:用 lambda 語法,建立要傳入 sorted 的參數函式

在看完上面兩個關鍵點後,相信你已經知道開頭的情境中,sorted 內的 lambda 實際機制:

basket_a = {"name": "a", "bananas": 25, "apples": 66, "mango": 16}
basket_b = {"name": "b", "bananas": 13, "apples": 74, "mango": 24}
basket_c = {"name": "c", "bananas": 35, "apples": 12, "mango": 30}

baskets = [
basket_a,
basket_b,
basket_c
]

sorted_by_banana = sorted(baskets, key=lambda basket: basket["bananas"])

for basket in sorted_by_banana:
print(basket)
# {'name': 'b', 'bananas': 13, 'apples': 74, 'mango': 24}
# {'name': 'a', 'bananas': 25, 'apples': 66, 'mango': 16}
# {'name': 'c', 'bananas': 35, 'apples': 12, 'mango': 30}
  1. 透過 lambda 建立了一個 function:輸入 basket dictionary 作為參數,回傳 basket dictionary 的 bananas 這個 key 的值。
  2. 建立好的 function 會以參數的形式,傳入 sorted function ,sorted function 會將串列中的每個 element 傳入到這個 lambda function 執行;取得的結果, 將是 element 判斷大小的依據。

以我們的示範為例 🌰,我們傳入的 lambda 為

「傳入 basket dictionary,並回傳這個 dictionary key 為 bananas 的值」

而 sorted function 就是根據每個 basket 內的 bananas 值來排序,從最少排到最多,順序即是:
basket_b (bananas=15) >> basket_a (bananas=25) >> basket_c (bananas=35)

同理,如果想要改用 apples 數量作為排序依據的話,把 lambda 的回傳值調整一下就可以囉!

sorted_by_apple = sorted(baskets, key=lambda basket: basket["apples"])

for basket in sorted_by_banana:
print(basket)

# {'name': 'c', 'bananas': 35, 'apples': 12, 'mango': 30}
# {'name': 'a', 'bananas': 25, 'apples': 66, 'mango': 16}
# {'name': 'b', 'bananas': 13, 'apples': 74, 'mango': 24}

至於要在什麼情境下該使用 lambda 函式來取代一般的函式

可以用下面兩個依據作為判斷:

  • 函式本身是否單純,只關注「傳入參數」與「回傳值」的關係?
    如果函式還有其他額外操作、不只是輸入與回傳的話,就不適合使用 lambda 來建立函式。
  • 傳入的參數函式是否僅使用這一次?
    以上述為例,如果還有其他地方也要用 bananas 作為回傳標準的話,就可以考慮把 lambda function 提取出來,而不是直接在 sorted 裏面建立,之後在把 function 名稱傳入 sorted 函式:
def sorting_rule(basket):
return basket["bananas"]

# ...code...
sorted_baskets = sorted(baskets, key=sorting_rule)

# ...code...
another_sorted_baskets = sorted(another_baskets, key=sorting_rule)

回家作業:你及格了嗎?

請觀察以下的程式碼:

class Student:
name: str
chinese_score: int
math_score: int
science_score: int
society_score: int

def __init__(self, name, chinese_score, math_score, science_score, society_score) -> None:
self.name = name
self.chinese_score = chinese_score
self.math_score = math_score
self.science_score = science_score
self.chinese_score = society_score

def __repr__(self) -> str:
return self.name


ming = Student("Ming", 69, 77, 99, 48)
mei = Student("May", 88, 75, 89, 91)
mike = Student("Mike", 92, 33, 45, 60)

my_students = [
ming,
mei,
mike,
]

def check_students_pass_the_exam(students, standard_score, standard_method):
for student in students:
score = standard_method(student)
if score >= standard_score:
print(f"Congratulation! {student.name} passed the exam!")
else:
print(f"Sorry, {student.name} failed the exam...")

在上面的程式碼中,我們定義了一個學生型別 Student ,其中包含姓名 (name)、數學成績 (math_score)、國文成績 (chinese_score)、自然成績 (science_score)、社會成績 (society_score) 等幾個科目分數的屬性。

而在 check_students_pass_the_exam 函式中,則是我們判斷學生們是否及格的函數:我會定義一個及格分數 standard_score,以及學生計算及格分數的函式 standard_method

假設今天我是數學老師,我要檢查每位學生的數學成績是否超過 60 分,就可以透過以下程式碼來達成:

check_students_pass_the_exam(my_students, 60, lambda student: student.math_score)

# Congratulation! Ming passed the exam!
# Congratulation! May passed the exam!
# Sorry, Mike failed the exam...

請你寫出以下情境中,你會如何設計你的程式呢?

  • 四科成績的平均分數不得低於 65 分
  • 數學成績與國文成績之和不得低於 150 分
  • 分數最低的學科分數不得低於 50 分
  • 社會與自然至少一科不得於 80 分

我們是 ccClub 團隊,致力於讓 Python 成為大家的第二外語,希望能用淺顯易懂、循序漸進的方式,帶領新手一步步跨入程式設計的世界。

如果你喜歡這篇文章,請給我們 1~10 個掌聲👏。
如果你喜歡「Py立得」的教學系列文,請給我們 21 個以上的掌聲👏。

Facebook: ccClub Python讀書會

--

--