เกริ่น
ถ้าพูดถึงการคำนวณกับ Array หรือ List แล้ว หลายคนน่าจะเริ่มต้นมากับ for loop, while do ที่ต้องมานั่งนับ index (แล้วก็คิดว่า length เราต้อง -1 มั้ยนะ) หลังจากนั้นก็เริ่มมี for each จนกระทั่ง map ที่ทำให้โค๊ดดูง่ายขึ้น ซับซ้อนน้อยลง ฯลฯ
แต่บทความนี้จะไม่พูดถึง for loop (ยาวไป หาอ่านได้ง่ายว่าทำไม) ขอเปรียบเทียบท่าที่พบได้ทั่วไปในภาษา Python คือ list comprehension และท่าที่น่าจะไม่ค่อยได้ใช้กันคือ map ว่าข้อดีของการใช้ map คืออะไร
Python กับการทำงานดาต้า
แน่นอนว่าในงาน Data Science, Machine Learning นั้น Python ถือได้ว่าเป็นภาษาหลักที่ทุกคนเลือกใช้ เพราะความง่ายของภาษา และเครื่องมือต่างๆ ที่มีมากมายให้เลือกใช้ เพื่อที่ Data Scientist ทุกคนจะได้โฟกัสที่งานหลักของตัวเองคือ คลีนดาต้า เอ้ย! สร้างโมเดลที่มีความแม่นยำตอบโจทย์
แต่การจะนำโมเดลที่สร้างขึ้นมาไปใช้งานก็ไม่ใช่เรื่องง่าย บางทีโมเดลก็มีความแม่นยำมากแต่นำไปใช้งานจริงไม่ได้เพราะว่าต้องใช้พลังในการคำนวณสูงไป หรือไม่มีคน deploy ให้ ทำให้ Data Scientist เองก็ต้องไปเขียน Backend เอง ซึ่งบางองค์กรจะมีตำแหน่ง Python Developer หรือ Machine Learning Engineer ที่โฟกัสการนำโมเดลไปใช้งานจริงมากกว่ามาช่วยตรงส่วนนี้
ถ้าระบบทำงานกับข้อมูลที่มีจำนวนน้อยจัดการได้ใน excel ก็อาจจะไม่เห็นความสำคัญตรงจุดนี้ แต่ในการทำงานจริงในยุคมากกว่า 4.0 นี้เราต้องจัดการกับข้อมูลมหาศาล เรามี cloud ที่สามารถ scale ได้ตามความต้องการ แต่ cost ของสิ่งเหล่านี้จะบวกตามขึ้นเรื่อยๆ การ refactor หรือ optimize ระบบก็ถือเป็นการลดต้นทุนที่ขาดไม่ได้ ตัวอย่างเช่น งานที่ process log ขนาด 500GB ทุกวัน ซึ่งเปลี่ยนไปใช้ภาษา Rust แทนก็ลด cost ไปได้กว่า 30,000 บาทต่อเดือน https://andre.arko.net/2018/10/25/parsing-logs-230x-faster-with-rust/
แต่เราคงไม่แปลงโมเดลอันแสนซับซ้อนไปเป็นภาษาอื่นเลย เบื้องต้นเราสามารถ refactor โค๊ด python ที่มีอยู่ให้ดีขึ้นไปอีก นอกจากนี้ข้อดีของการ refactor อื่นๆ ไม่ว่าจะเป็น testability, scalability, maintainability, readability ไม่ใช่ว่าผ่านไป 3 เดือนเป็นอื่น กลับมาอ่านโค๊ดแล้วจำไม่ได้ สิ่งเหล่านี้ map สามารถมาตอบโจทย์ให้เราได้
map vs list comprehension
สมมติว่าผู้อ่านรู้วิธีเขียน map และ list comprehension ในภาษา python อยู่แล้ว
เตรียมการทดลองสุ่ม list integer ระหว่าง 0–9999 ขนาด 10 ล้าน
ทดลอง +1 ทุกค่าใน list จะเห็นได้ว่า list comprehension จะเร็วกว่า map พอสมควร สังเกตว่าเราต้องแปลง map เป็น list เพราะ map เป็น Lazy evaluation คือยังไม่คำนวณเลยในทันที
ถ้าเราไม่ใช้คำสั่ง list จะเห็นได้ว่าสิ่งที่เราได้ออกมาคือ map object เท่านั้น
แต่ในความเป็นจริงเราจะไม่ค่อยใช้ x + 1 ตรงๆ ข้างใน list comprehension เพราะอาจจะมีการคำนวณที่ยาวและซับซ้อน การแยกการคำนวณมาเป็นฟังก์ชั่นด้านนอกจะช่วยให้จัดการง่ายมากกว่า
กลายเป็นว่าพอเราทำงานกับฟังก์ชั่น list comprehension กลับทำงานได้ช้าลงมาก ยิ่งถ้ามีหลาย step ก็ยิ่งเห็นความต่างมากขึ้น ดังกราฟด้านล่างที่มีรันฟังก์ชั่น add1 ตั้งแต่ 1 จนถึง 10 รอบ
เราอาจจะรวมหลายฟังก์ชั่นเป็นตัวเดียวก็ได้เพื่อลดการเรียก list comprehension หลายรอบ แต่ก็จะมีกรณีที่เราต้องการ filter ข้อมูลที่ไม่ต้องการออกก็จะกลายเป็นต้องเรียกหลายรอบอยู่ดีดังตัวอย่างด้านล่าง
Iteration Direction
ลำดับการทำงานของ list comprehension กับ map จะมีความต่างกัน จากตัวอย่าง
ทดสอบโดยการแทรกคำสัง print ค่าระหว่างเรียก function
จะเห็นได้ว่า list comprehension จะรันทีละ list ให้เสร็จก่อน แต่ map จะทำการคำนวณทีละตำแหน่งให้เสร็จก่อน ซึ่งพฤติกรรมแบบนี้ส่งผลดีหลายอย่าง
สมมติว่าตอนพัฒนาเราไม่ได้คิดถึงเคสที่ทำให้ error ได้
ถ้าขนาด list ของเรายาวมากกว่าที่เราจะเห็น bug ที่เราให้กำเนิดไว้ก็ต้องรอ step ก่อนๆหน้าเสร็จก่อน แต่ถ้าใช้ map ก็จะเจอ fail ได้ fast กว่า
หรืออาจจะใช้ map เป็น generator ก็ได้
- ถ้าเรามีฟังก์ชั่นกลางทางชื่อ bigBuff ที่จะสร้าง ตัวแปรขนาด 50MB ต่อ 1 element แต่ปลายทางเราแปลง 50MB กลายเป็น 1KB อยู่ดี
- ลองคิดว่าถ้า list เรายาว 1000 แล้วใช้ list comprehension หมายความว่าเราต้องใช้ RAM 50GB! สำหรับ buffer กลางทาง
- แต่พอใช้ map เราสนใจเก็บแค่ปลายทางเท่านั้น เพราะการคำนวณเกิดทีละ element
ทั้งหมดทั้งมวลนี้ถ้าใช้ for loop ก็ จัดการได้เหมือนกัน (อ้าว!) ซึ่งสาเหตุที่ไม่ควรใช้ for loop นอกจากที่จะเกริ่นไปแล้วว่า (หาอ่านเองได้ว่าทำไม) ก็คือ สมมติว่ามีการทำงานนึงมี step ตามด้านล่าง
ใน step ที่รันฟังก์ชั่น fxckLongIntensiveCompute
เป็นฟังก์ชั่นที่ต้องใช้ CPU ในการคำนวณเยอะมากและเราควร parallel ด้วยทุก CPU Core ที่เรามีอยู่ เราเพียงแก้เพิ่มว่า
เท่านี้ฟังก์ชั่นนี้ก็จะรันแบบ parallel ถ้ามาเป็น for loop ก็ต้องมานั่งไล่โค๊ดดูกันละเอียดว่าทำได้มั้ยนะ (จริงๆ map ก็ต้องไล่ดูกรณีมีพวก side effect พวก io ทั้งนี้เป็นเรื่องของ pure function ใน functional programming)
สรุป
ใช้ map เถอะ
おわり。