map(), filter(), reduce() และฟังก์ชันอันดับสูงใน Python
บทเรียนนี้แปลจากต้นฉบับภาษาอังกฤษRead in EnglishLeer en españolЧитать на русскомLer em portuguêsแจ้งข้อผิดพลาดในการแปล
ลองนึกภาพสายการผลิตในโรงงาน วัตถุดิบเข้ามาทางปลายด้านหนึ่ง ที่สถานีแรก ชิ้นงานแต่ละชิ้นถูกขึ้นรูป ที่สถานีถัดไป ชิ้นงานที่บกพร่องถูกคัดออก และที่สถานีสุดท้าย ชิ้นงานดีทั้งหมดถูกประกอบรวมเป็นผลิตภัณฑ์สำเร็จหนึ่งชิ้น
Python มีฟังก์ชันสามตัวที่ทำงานเหมือนสายการผลิตนี้ทุกประการ: map() แปลงข้อมูลทุกตัว filter() เก็บไว้เฉพาะข้อมูลที่ต้องการ และ reduce() รวมทุกอย่างให้เหลือผลลัพธ์เดียว เมื่อใช้ร่วมกัน ฟังก์ชันเหล่านี้ช่วยให้ประมวลผลกลุ่มข้อมูลได้อย่างเป็นระเบียบและอ่านง่าย
ในบทเรียนนี้ คุณจะได้เรียนรู้ว่าเครื่องมือเหล่านี้ทำงานอย่างไร ควรใช้เมื่อไร และเมื่อไรที่ list comprehension (การสร้างลิสต์แบบย่อ) อาจเป็นทางเลือกที่ดีกว่า เมื่อจบบทเรียน คุณจะเขียนไปป์ไลน์ (pipeline) ประมวลผลข้อมูลได้ดีจนหัวหน้าคนงานในโรงงานยังต้องภูมิใจ
ฟังก์ชันอันดับสูงคืออะไร
ฟังก์ชันอันดับสูง (higher-order function) คือฟังก์ชันที่รับฟังก์ชันอื่นเป็นอาร์กิวเมนต์ หรือคืนค่าออกมาเป็นฟังก์ชัน แค่นั้นเอง
คุณเคยใช้มาแล้วโดยไม่รู้ตัว ฟังก์ชันในตัว sorted() มีพารามิเตอร์ key ที่ให้ส่งฟังก์ชันเข้าไป:
['apple', 'banana', 'cherry']
ในตัวอย่างนี้ len ถูกส่งเป็นอาร์กิวเมนต์ให้ sorted() ฟังก์ชัน sorted จะเรียก len กับข้อมูลแต่ละตัวเพื่อตัดสินลำดับ คุณไม่ได้เรียก len() เอง เพียงแค่บอก sorted ว่าจะใช้ฟังก์ชันไหน
map(), filter() และ reduce() ทำงานแบบนี้ทั้งหมด คุณส่งฟังก์ชันและกลุ่มข้อมูลให้ แล้วฟังก์ชันเหล่านี้จะนำฟังก์ชันที่ได้รับไปใช้กับข้อมูลแต่ละตัว นี่คือสิ่งที่ทำให้ทั้งสามเป็นฟังก์ชันอันดับสูง
map(): แปลงข้อมูลทุกตัว
map() รับฟังก์ชันและออบเจกต์ที่วนซ้ำได้ (iterable) เช่น ลิสต์ แล้วนำฟังก์ชันไปใช้กับข้อมูลทุกตัวและคืนผลลัพธ์ที่แปลงแล้ว ให้นึกถึงมันเป็น "สถานีขึ้นรูป" ในสายการผลิตของเรา
รูปแบบคือ map(function, iterable) ซึ่งคืนค่าเป็นออบเจกต์ map ที่ปกติจะนำไปแปลงเป็นลิสต์
[2, 4, 6, 8, 10]
map(double, numbers) เรียก double(1), double(2), double(3) ไปเรื่อย ๆ และเก็บผลลัพธ์ทั้งหมดไว้ ส่วน list() ที่ครอบอยู่จะแปลงออบเจกต์ map เป็นลิสต์ธรรมดาเพื่อให้แสดงผลได้
การเขียนฟังก์ชันทั้งตัวเพียงเพื่อคูณเลขด้วยสองดูเกินจำเป็น จึงมักใช้ map() คู่กับ lambda ซึ่งเป็นฟังก์ชันเล็ก ๆ ที่ไม่มีชื่อ:
[2, 4, 6, 8, 10] ['ALICE', 'BOB', 'CHARLIE'] [10, 20, 6]
คุณส่งฟังก์ชันใดก็ได้ให้ map() ทั้งฟังก์ชันที่เขียนเอง lambda หรือฟังก์ชันในตัว เช่น str.upper, round, len หรือ int
filter(): เก็บไว้เฉพาะสิ่งที่ต้องการ
filter() รับฟังก์ชันและ iterable ฟังก์ชันต้องคืนค่า True หรือ False สำหรับข้อมูลแต่ละตัว และจะเก็บไว้เฉพาะตัวที่ฟังก์ชันคืนค่า True ให้นึกถึงมันเป็นฝ่ายควบคุมคุณภาพในสายการผลิตที่คัดชิ้นงานบกพร่องออกไป
[2, 4, 6, 8, 10]
filter(is_even, numbers) เรียก is_even(1) ซึ่งคืนค่า False (1 จึงถูกตัดทิ้ง) is_even(2) คืนค่า True (2 จึงถูกเก็บไว้) ไปเรื่อย ๆ เฉพาะข้อมูลที่ผ่านการทดสอบเท่านั้นที่จะเหลืออยู่
['hello', 'world', 'python'] ['hello', 'world', 'python'] [2, 5, 8]
การส่ง bool ให้ filter() เป็นเทคนิคที่เข้าท่า สตริงว่าง ศูนย์ None และลิสต์ว่าง ล้วนเป็นค่า "falsy" ใน Python คือ bool() คืนค่า False สำหรับค่าเหล่านี้ ดังนั้น filter(bool, items) จะตัดค่า falsy ทั้งหมดออก
ใช้ map() และ filter() ร่วมกัน
พลังที่แท้จริงมาจากการต่อฟังก์ชันเหล่านี้เป็นทอด ๆ ใช้ filter ก่อนเพื่อเลือกข้อมูล แล้วใช้ map เพื่อแปลงข้อมูล เหมือนสถานีต่าง ๆ ในสายการผลิต
[87, 96, 72, 78, 93, 100, 65]
เนื่องจากทั้ง filter() และ map() ทำงานแบบ lazy คุณจึงต่อกันได้โดยไม่ต้องสร้างลิสต์ระหว่างทาง ข้อมูลไหลผ่านไปป์ไลน์ได้อย่างมีประสิทธิภาพ
ต่อไปเป็นตัวอย่างที่ใกล้เคียงการใช้งานจริงมากขึ้น คือการประมวลผลลิสต์ราคาสินค้า:
[10.39, 71.99, 36.0, 120.0]
reduce(): รวมข้อมูลทุกตัวให้เหลือค่าเดียว
reduce() รับฟังก์ชันและ iterable แล้วรวมข้อมูลทุกตัวให้เหลือค่าเดียว นี่คือสถานีสุดท้ายในสายการผลิต ที่ชิ้นส่วนทั้งหมดถูกประกอบเป็นผลิตภัณฑ์หนึ่งชิ้น
reduce() ไม่ใช่ฟังก์ชันในตัว ต่างจาก map() และ filter() คุณต้อง import มาจากโมดูล functools
Sum: 15 Product: 120
ต่อไปนี้คือการทำงานของ reduce() ทีละขั้นสำหรับการหาผลรวม ฟังก์ชันรับอาร์กิวเมนต์สองตัว คือผลลัพธ์สะสมจนถึงตอนนี้ (a) และข้อมูลตัวถัดไป (b):
a=1, b=2 ได้ 3a=3, b=3 ได้ 6a=6, b=4 ได้ 10a=10, b=5 ได้ 15Longest word: hippopotamus Alice & Bob & Charlie
map/filter กับ list comprehension
Python มีอีกวิธีในการแปลงและกรองลิสต์ คือ list comprehension ซึ่งมักให้โค้ดที่อ่านง่ายกว่า map() และ filter() ลองมาเปรียบเทียบกัน
numbers = [1, 2, 3, 4, 5, 6]
# Double even numbers
evens = filter(lambda x: x % 2 == 0, numbers)
result = list(map(lambda x: x * 2, evens))
print(result) # [4, 8, 12]numbers = [1, 2, 3, 4, 5, 6]
# Double even numbers
result = [x * 2 for x in numbers if x % 2 == 0]
print(result) # [4, 8, 12]list comprehension ทำทั้งการกรองและการแปลงได้ในบรรทัดเดียวที่อ่านง่าย สำหรับการแปลงแบบง่าย ๆ comprehension มักเป็นทางเลือกที่ดีกว่าใน Python
แล้วเมื่อไรควรใช้ map() และ filter() ทั้งสองมีประโยชน์เมื่อคุณมีฟังก์ชันที่มีชื่ออยู่แล้วและต้องการนำไปใช้:
['Alice', 'Bob', 'Charlie'] ['Alice', 'Bob', 'Charlie']
แบบฝึกหัด
ถึงเวลาฝึกแล้ว แบบฝึกหัดเหล่านี้เริ่มจากการใช้ฟังก์ชันทีละตัว แล้วค่อย ๆ ไปสู่การนำมาต่อกันเป็นไปป์ไลน์ อย่าลืมครอบ map() และ filter() ด้วย list() เมื่อต้องการแสดงผล
ใช้ map() กับ lambda เพื่อยกกำลังสองตัวเลขทุกตัวในลิสต์ [1, 2, 3, 4, 5]
แสดงผลลัพธ์เป็นลิสต์
ผลลัพธ์ที่ต้องการ:
[1, 4, 9, 16, 25]ใช้ filter() กับ lambda เพื่อเก็บไว้เฉพาะคำที่มีมากกว่า 3 อักขระ
ข้อมูลที่ให้: ["hi", "hello", "hey", "world", "ok", "python"]
แสดงผลลัพธ์เป็นลิสต์
ผลลัพธ์ที่ต้องการ:
['hello', 'world', 'python']อ่านโค้ดนี้แล้วทายผลลัพธ์ จากนั้นเขียนคำสั่ง print() ที่ให้ผลลัพธ์เหมือนกันทุกประการ
numbers = [10, 25, 30, 45, 50]
result = list(map(lambda x: x // 10, filter(lambda x: x >= 30, numbers)))
print(result)คิดทีละขั้น: filter เก็บอะไรไว้ แล้ว map ทำอะไรกับข้อมูลแต่ละตัว
โค้ดนี้ควรแปลงลิสต์ของสตริงอุณหภูมิอย่าง ["32F", "100F", "212F"] ให้เป็นจำนวนเต็มในหน่วยองศาเซลเซียส แต่มีบั๊กอยู่สองจุด
ผลลัพธ์ที่ต้องการ:
[0, 38, 100]สูตร: เซลเซียส = (ฟาเรนไฮต์ - 32) * 5 / 9 แล้วปัดเป็นจำนวนเต็มที่ใกล้ที่สุด
ใช้ reduce() จากโมดูล functools เพื่อคำนวณผลคูณของตัวเลขทั้งหมดในลิสต์ [2, 3, 4, 5]
ผลคูณคือ 2 * 3 * 4 * 5 = 120
แสดงผลลัพธ์
ผลลัพธ์ที่ต้องการ:
120จากลิสต์คะแนนของนักเรียน ให้สร้างไปป์ไลน์ที่:
1. กรองคะแนนที่สอบตกออก (เก็บไว้เฉพาะคะแนน >= 60)
2. เพิ่มคะแนนที่สอบผ่านแต่ละตัวอีก 10 คะแนน (ใช้ map)
3. จำกัดคะแนนไม่ให้เกิน 100 (ใช้ map กับ min)
ข้อมูลที่ให้: [45, 82, 55, 91, 67, 38, 73, 95]
แสดงลิสต์สุดท้าย
ผลลัพธ์ที่ต้องการ:
[92, 100, 77, 83, 100]คำใบ้: ถ้าจะจำกัดไม่ให้เกิน 100 ให้ใช้ min(score, 100)
ปรับโครงสร้าง (refactor) ไปป์ไลน์ map/filter นี้ให้เป็น list comprehension เดียวที่ให้ผลลัพธ์เหมือนกัน
โค้ดเดิม:
words = ["Hello", "WORLD", "Python", "CODE", "test"]
result = list(map(str.lower, filter(lambda w: len(w) > 4, words)))เขียน list comprehension ที่ให้ผลลัพธ์เดียวกันแล้วแสดงผล
ผลลัพธ์ที่ต้องการ:
['hello', 'world', 'python']สรุป
สรุปสิ่งที่ได้เรียนรู้เกี่ยวกับเครื่องมือเชิงฟังก์ชันใน Python:
| ฟังก์ชัน | หน้าที่ | คืนค่า |
|---|---|---|
map(fn, iterable) | แปลงข้อมูลทุกตัว | ออบเจกต์ map (lazy) |
filter(fn, iterable) | เก็บข้อมูลที่ fn คืนค่า True | ออบเจกต์ filter (lazy) |
reduce(fn, iterable) | รวมข้อมูลทุกตัวให้เหลือค่าเดียว | ค่าเดียว |
| ฟังก์ชันอันดับสูง | ฟังก์ชันที่รับหรือคืนค่าเป็นฟังก์ชัน | ขึ้นอยู่กับฟังก์ชันนั้น |
ข้อสรุปเชิงปฏิบัติ: ใช้ list comprehension กับงานแปลงหรือกรองข้อมูลส่วนใหญ่ใน Python เลือกใช้ map() และ filter() เมื่อมีฟังก์ชันที่มีชื่อให้นำไปใช้ หรือต้องการการประมวลผลแบบ lazy และใช้ reduce() ให้น้อย เพราะฟังก์ชันในตัวของ Python รองรับกรณีส่วนใหญ่ได้ดีกว่า
เรียนอะไรต่อ
ตอนนี้คุณมีพื้นฐานเรื่องฟังก์ชันใน Python ที่แข็งแรงแล้ว ต่อไปลองศึกษา การจัดการข้อผิดพลาด เพื่อเรียนรู้วิธีเขียนโค้ดที่รับมือกับความผิดพลาดได้อย่างราบรื่นแทนที่จะหยุดทำงาน