Tutorial

Pydash ร่างทรงของ Lodash ใน Python

By Arnon Puitrakul - 19 กรกฎาคม 2021

Pydash ร่างทรงของ Lodash ใน Python

หลาย ๆ คนที่เขียน Javascript มา น่าจะเคยผ่านการใช้งาน Library อย่าง Lodash มาไม่มากก็น้อย ไม่สิ ตอนนี้เรามาใช้ Underscore.js แล้วป่ะ หรือว่าเราโบราณไปแล้ว ฮ่า ๆ ไม่ได้เขียนนาน แต่นั่นแหละ ถ้าเราได้ใช้ Library พวกนี้ไปแล้วมาเขียน Python ละ มันมี Library อะไรที่ทำเหมือน Lodash มั้ย คำตอบคือ มี แต่เราเรียกว่า Pydash วันนี้เรามาดูกันว่ายังไง

ติดตั้ง Pydash

pip install pydash

การติดตั้ง Pydash ไม่ใช่เรื่องยากเลย เราสามารถติดตั้งผ่าน Pip ตรง ๆ ได้เลย และสำหรับ Apple Silicon ก็ไม่ต้องห่วง เราลองบน Macbook Air M1 แล้ว ก็คือลงได้ แต่เราใช้ Conda Forge ก็ได้เลย แต่เราลองลงผ่าน Pip แล้วมันไม่รอด ก็งง เหมือนกัน ดังนั้น ถ้าใช้ Apple Silicon ณ วันที่เขียนพยายามใช้ Conda Forge น่าจะดีกว่า

from pydash import py_ as _

เวลาเราจะใช้งาน เราต้อง Import มันเข้ามาก่อนผ่านสำสั่งด้านบน จะเห็นว่า ด้านหลังเราใส่ as เพื่อบอก Alias ของมันลงไปด้วย เป็นเครื่องหมาย Underscore จริง ๆ ไม่ต้องใส่ก็ได้ แต่เราเคยชินกับ Underscore.js ที่ใส่แบบนั้นจนชินไปซะแล้ว เลยเออ ไหน ๆ ก็เอามาใส่ละกัน

สำหรับคนที่ย้ายมาจาก Lodash

จริง ๆ ใน Pydash มีอะไรให้เราเล่นเยอะมาก ๆ เหมือนพวก Lodash เลย Function อะไรที่ Lodash มี Pydash จะมีเหมือนกันเลย แต่แค่การตั้งชื่อจะไม่เหมือนกันเท่านั้น เช่น ใน Python เราใช้ Snake Case ดังนั้น ชื่อ Function ของ Pydash ก็จะเป็น Snake Case เท่านั้น

นอกจากนั้น มันจะมีบาง Function ที่ชื่อมันจะไปซ้ำกับ Built-in Function ของ Python อยู่แล้ว ก็จะแก้ปัญหาด้วยการเติม Underscore ไว้ข้างหลังก็จะไปเรียกของ Pydash แล้ว ดังนั้นคนที่ใช้พวก Lodash มาก่อน ไม่ต้องมานั่งหาแล้วว่า มันทำอะไรได้บ้าง เพราะมันทำได้คล้ายกันมาก ๆ เลย แค่เปลี่ยนชื่อเท่านั้นเอง

Function ที่ใช้บ่อย ๆ

[1,2,[3,4],[5,6]]

ลองมาดูพวก Function ที่เราใช้งานกันบ่อย ๆ บ้างว่าใช้อันไหนบ่อยมาก ๆ เริ่มจากเรื่องของ List กันก่อน ถ้าเราบอกว่า เรามี List ที่ข้างในก็มี List อยู่ และ เราต้องการที่จะเอาพวก List ที่อยู่ด้านในออก แล้วรวม Element เข้าด้วยกัน ดังตัวอย่างด้านบน เราจะทำยังไงดี ถ้าเราไม่มี Pydash เราก็ต้องเขียนเองใช่ป่ะ

data = [1,2,[3,4],[5,6]]
result_list = []

for item in data :
	if type(item) != list :
    	result_list.append(item)
    else:
    	result_list += item

อะ เราทำขำ ๆ ให้ดูละกัน ถ้าเกิดเราเจอเคสดั่งตัวอย่าง ถ้าเราอยากจะรวบมันให้เป็น List เดียวเลย เราก็ต้องค่อย ๆ Loop หาไปเรื่อย ๆ แล้วว่า ถ้า Item เป็นของที่ไม่ใช่ List เราก็แค่ Append หรือจับต่อท้ายไป กลับกัน ถ้าเป็น List เราก็บวกมันเข้าไปเลย มันก็คือ การต่อ List ใน Python แค่นั้นเลย แต่จะเห็นว่า กว่าเราจะได้มันมา เราใช้หลายบรรทัดมาก ๆ และ เมื่อเราเขียนแบบนี้ไปเยอะ ๆ แล้ว การจัดการ Code มันจะยุบยับมาก ๆ Code มันจะอ่านไม่ค่อยรู้เรื่องไม่เป็น Functional เท่าไหร่

result_list = _.flatten_deep(data)

แต่ถ้าเราใช้ Pydash เขา Implement Function สำหรับการรวบ List ไว้ให้เราแล้ว เราก็แค่ไปเรียกมาใช้ ก็จะใช้งานได้เลยทันที ถือว่า ทำให้ Code ของเราดูเรียบง่ายมากขึ้น เน้นความเป็น Functional มากขึ้นดูดีขึ้นเยอะ

[1,2,3,4,5,6] -> [[1,2,3],[4,5,6]]

หรืออีกงานที่ก็ต้องทำบ่อย คือการซอย List ออกเป็น Chunk ตัวอย่างที่เราใช้บ่อย ๆ เราจะใช้กับเวลาเราทำงานกับข้อมูลใหญ่ ๆ แล้วเราจะแบ่ง Chunk ข้อมูลแล้วโยนให้ Joblib โยนไปที่แต่ละ Process ถ้าเป็นเมื่อก่อน เราก็ต้องเขียนเองเลย จะเป็นเหมือนด้านล่างนี้

data = [1,2,3,4,5,6]
result = []

chunk_size = 3

for start_position in range(0,len(data), chunk_size) :
	result.append(data[start_position:start_position+chunk_size]

// List Comprehension
result = [data[start_position:start_position+3] for start_position in range(0, len(data), chunk_size)]

Code ที่เราเขียน เราก็ทำง่าย ๆ เลย เราก็เล่น Loop ไปเลย ทีละ Chunk เช่น ตำแหน่งแรก เราต้องเอาตำแหน่งที่ 0-3 เราก็ Loop ตำแหน่งเริ่ม แล้วก็ Slice List ถึงตำแหน่งเริ่มบวกด้วยขนาดของ Chunk เท่านี้เราก็จะได้แบบที่เราต้องการแล้ว หรือ ถ้าเราอยากจะย่อให้สั้นลง เราก็สามารถใช้ List Comprehension ได้ แต่ไม่ว่า Code เราจะเขียนแบบไหนก็ยังยุ่งยากอยู่ดี ลองเอา Pydash มาเสียบดีกว่า

result = _.chunk(data,3)

บรรทัดเดียวจบ ง่ายมาก ๆ เลย เพราะเขาเขียนมาให้เราแล้วนั่นเองฮ่า ๆ หรือจริง ๆ อีกเรื่องที่เราใช้บ่อยมาก ๆ ทั้งใน Lodash และ Pydash คือคำสั่ง Get โดยเฉพาะในเหตุการณ์อย่าง เราเรียก API มา ได้ค่ากลับมาเป็น JSON แล้วบางที มันจะคืนโครงสร้างของ JSON กลับมาไม่เหมือนกัน เช่น Error อะไรแบบนี้จะไม่มีถ้าไม่ Error อะไรแบบนั้น หรือ ช่องของตัวเลขบางอย่างมันจะไม่มีในบางสถานการณ์ ซึ่งวิธีการจัดการง่าย ๆ คือ เราก็ต้องเขียน Logic สำหรับเข้ามาเช็ค และอาจะให้ Default Value อะไรก็ว่าไป ถ้าเราเขียนแค่อันเดียว อาจจะไม่น่าเบื่อมาก แต่อย่าลืมว่า เวลาเรารับค่ากัน เราไม่ได้รับกันตัวเดียวสักหน่อย เรารับกันเป็นร้อย ไหนจะแต่ละงานที่เราทำอีก เราต้องเสียเวลาเขียนพวกนี้ขนาดไหน

if 'error' not in result :
	error_msg = 'No Error'
else:
	error_msg = result['error_msg']

เวลาเราเขียนเช็ค เราก็ต้องเขียนอารมณ์แบบนี้แหละ อาจจะย่อให้มันสั้นได้ แต่ถ้าเราใช้ Pydash เราสามารถจบได้ที่บรรทัดเดียว ต่อ 1 ค่าได้เลย

error_msg = _.get(result, 'error', 'No Error')

เพียงคำสั่งเดียว เราก็จัดการได้ค่าแบบที่เราต้องการมาอย่างรวดเร็วทันใจเลย

Chain & Lazy Evaluation

แล้วมันสามารถทำอะไรที่พีคกว่านั้นได้อีก เช่นการทำ Chain Evaluation และ Lazy Evaluation ลองมาดูแบบที่เป็น Chain Evaluation กันก่อนจากตัวอย่างด้านล่าง

result = _([1, 2, 3, 4]).without(2, 3).reject(lambda x: x > 1).value()

เราจะเห็นว่า เรา Pass List ลงไปใน Object ของ Pydash แล้วพ่วงด้วย Function without ซึ่งก็คือไม่เอา 2 และ 3 สุดท้ายก็ reject เราก็ใส่เป็น Lambda ก็คือ อะไรที่มากกว่า 1 เอาออกไป สุดท้าย value() ก็คือให้มัน Evaluate ทั้งหมดออกมา ดังนั้น เราบอกว่า เราไม่เอา 2 และ 3 แล้วไม่เอาอะไรที่มากกว่า 1 ด้วย ดังนั้นผลลัพธ์ของ Function นี้ก็คือ [1] เฉย ๆ นั่นเอง

>> _([1, 2, 3, 4]).without(2, 3).reject(lambda x: x > 1)
<pydash.chaining.Chain object at 0x10c709d60>

ถ้าเกิดว่า เราไม่เรียก value() ตอนสุดท้ายละ มันจะเกิดอะไรขึ้น คำตอบก็คือ Expression ที่เราต่อกันมาทั้งหมดก็จะไม่ทำงานนั่นเอง ถ้าเราลอง Print ออกมาดู มันจะบอกว่า มันเป็น Object ตัวนึงของ Pydash นั่นเอง และ ถ้าเราต้องการคำตอบแล้ว เราก็เอามันไปเรียก value() มันก็จะ Evaluate ค่าออกมาให้เรานั่นเอง การทำแบบนี้ เราจะเรียกว่า การทำ Lazy Evaluation มีประโยชน์มาก ๆ กับข้อมูลใหญ่ ๆ อันนี้อาจจะยังไม่เห็นภาพ เพราะสุดท้าย เราก็เรียก value() ให้มัน Evaluate ออกมาทั้งหมดเลย สุดท้ายมันก็จะไม่ต่างจากการที่เราเรียกปกติ งั้นลองใหม่ ทำเหมือนเดิมเลย

>> _([list(range(0,10000))]).without(2, 3).reject(lambda x: x > 1).for_each(print)

อันนี้เราใช้ตัวอย่างเดิมละกัน แต่เราเพิ่มขนาดของตัวเลขเข้าไป จาก 4 หลายเป็น 10,000 ไปเลย สมมุติว่าเราทำงานกับข้อมูลขนาดใหญ่มาก ๆ เราไม่สามารถ Fetch ทั้งหมดลงใน Memory แล้วเราทำงานต่อกับมันได้ เราก็น่าจะต้องค่อย ๆ Fetch มันขึ้นมาทำงานไปเรื่อย ๆ นี่แหละ ประโยชน์ของ Lazy Evaluation มันสั่งให้เครื่องค่อย ๆ ทำออกมาเรื่อย ๆ มากกว่าการทำทีละงานแล้วปล่อยผลลัพธ์ออกมาตูมเดียวเลย

Late Value Passing & Planting Value

Late Value Passing ก็เป็นอีกตัวที่เราชอบเหมือนกัน คือ เราสามารถ Reuse Chain ที่เราทำงานไว้ได้ ถ้าเป็นปกติ เราอาจจะต้องไป Define Function มาเป็นเรื่องเป็นราว เยอะไปหมด แต่อันนี้ เราสามารถสร้าง Chain เปล่า ๆ ไว้ แล้ว เราค่อยเอาค่ามาใส่ทีหลังได้ เราเลยเรียกว่า Late Value Passing นั่นเอง

square_sum = _().power(2).sum()

a = square_sum([1, 2, 3])
b = square_sum([4, 5, 6])

ตัวอย่างด้านบนนี้ เราจะเห็นได้ว่า เราสร้าง Chain ที่รับค่ามา แล้วยกกำลัง 2 และ เอามาบวกกัน แต่ด้านหน้า เราไม่ได้ใส่อะไรไว้เลย เพราะเรากะว่า จะใส่ทีหลัง หรืออยากจะ Reuse Chain นี้หลาย ๆ ครั้ง เราก็สร้างไว้เป็นตัวแปรเก็บไว้ แล้วพอเราอยากใช้ เราก็สามารถที่จะเรียกมันขึ้นมาได้เลย หรือ จริง ๆ แล้วสามารถต่อ Chain ไปได้เรื่อย ๆ เลยก็ไม่ว่ากัน ก็ทำให้การเขียนมันยืดหยุ่นขึ้นเยอะ แต่จากที่เราเขียนมา เราไม่ค่อยชอบการทำ Chain ซ้อน ๆ กัน เพราะเวลาเรา Trace มันจะยากหน่อย ถ้าเราเขียนทีเดียวเลย เออ มันก็ง่ายหน่อย ไม่ค่อยเสียเวลา Trace มากเท่าไหร่

square_sum = _([1, 2, 3]).power(2).sum()

a = square_sum.value()
b = square_sum.plant([4, 5, 6]).value()

เมื่อกี้เราลองยัดค่าเข้าไปทีหลัง เพื่อ Reuse Chain แต่ถ้าเรายัดค่าตั้งแต่ต้นแล้วละ เราจะ Reuse มันยังไง คำตอบก็คือการ Replant Value กลับเข้าไป ผ่านคำสั่ง Plant ลองดูในตัวอย่างด้านบน เราใส่ค่าไปแล้ว และเก็บ Chain ไว้ แล้วเรียก value() ใน a และ b เราอยากจะ Reuse Chain แต่มันใส่ค่าไปแล้ว เราก็เลยต้องเรียก plant เพื่อ Replant Value ลงไปก็ได้เหมือนกัน แล้วก็เรียก value() เพื่อ Evaluate chain ใหม่

สรุป

Pydash เป็น Library ที่เหมือนกับเป็นอุปกรณ์ในห้องครัว พวกมีดเอย เขียงเอยอะไรแบบนั้น เป็นของที่เราใช้ทำกับข้าวทุกครั้ง แต่ถ้าเราต้องไปซื้อใหม่ทุกครั้งมันก็ไม่น่าจะดีเท่าไหร่ ถ้าเราใช้ของที่เขาทำมาแล้ว ไม่ต้องไปซื้อเพิ่มทุกครั้ง มันก็เหมือนกับเราเรียก Pydash ขึ้นมาเลย แล้วก็ทำงานได้เลย โดยที่ไม่ต้องมานั่ง Reimplement Function พวกนี้เลย นอกจากนั้น มันยังทำให้โปรแกรมของเราอ่านง่ายเข้าใจว่ามันทำอะไร หรือจริง ๆ แล้วเราสามารถรวบ แล้วเขียนเป็น Functional Programming เลยก็ยังได้ รวม ๆ แล้วเราว่ามันทำให้เราทำงานได้เร็ว และ ยืนหยุ่นขึ้นนั่นเอง ทำให้เราชอบ

Read Next...

จัดการข้อมูลบน Pandas ยังไงให้เร็ว 1000x ด้วย Vectorisation

จัดการข้อมูลบน Pandas ยังไงให้เร็ว 1000x ด้วย Vectorisation

เวลาเราทำงานกับข้อมูลอย่าง Pandas DataFrame หนึ่งในงานที่เราเขียนลงไปให้มันทำคือ การ Apply Function เข้าไป ถ้าข้อมูลมีขนาดเล็ก มันไม่มีปัญหาเท่าไหร่ แต่ถ้าข้อมูลของเราใหญ่ มันอีกเรื่องเลย ถ้าเราจะเขียนให้เร็วที่สุด เราจะทำได้โดยวิธีใดบ้าง วันนี้เรามาดูกัน...

ปั่นความเร็ว Python Script เกือบ 700 เท่าด้วย JIT บน Numba

ปั่นความเร็ว Python Script เกือบ 700 เท่าด้วย JIT บน Numba

Python เป็นภาษาที่เราใช้งานกันเยอะมาก ๆ เพราะความยืดหยุ่นของมัน แต่ปัญหาของมันก็เกิดจากข้อดีของมันนี่แหละ ทำให้เมื่อเราต้องการ Performance แต่ถ้าเราจะบอกว่า เราสามารถทำได้ดีทั้งคู่เลยละ จะเป็นยังไง เราขอแนะนำ Numba ที่ใช้งาน JIT บอกเลยว่า เร็วขึ้นแบบ 700 เท่าตอนที่ทดลองกันเลย...

Humanise the Number in Python with "Humanize"

Humanise the Number in Python with "Humanize"

หลายวันก่อน เราทำงานแล้วเราต้องการทำงานกับตัวเลขเพื่อให้มันอ่านได้ง่ายขึ้น จะมานั่งเขียนเองก็เสียเวลา เลยไปนั่งหา Library มาใช้ จนไปเจอ Humanize วันนี้เลยจะเอามาเล่าให้อ่านกันว่า มันทำอะไรได้ แล้วมันล่นเวลาการทำงานของเราได้ยังไง...

ทำไม 0.3 + 0.6 ถึงได้ 0.8999999 กับปัญหา Floating Point Approximation

ทำไม 0.3 + 0.6 ถึงได้ 0.8999999 กับปัญหา Floating Point Approximation

การทำงานกับตัวเลขทศนิยมบนคอมพิวเตอร์มันมีความลับซ่อนอยู่ เราอาจจะเคยเจอเคสที่ เอา 0.3 + 0.6 แล้วมันได้ 0.899 ซ้ำไปเรื่อย ๆ ไม่ได้ 0.9 เพราะคอมพิวเตอร์ไม่ได้มองระบบทศนิยมเหมือนกับคนนั่นเอง บางตัวมันไม่สามารถเก็บได้ เลยจำเป็นจะต้องประมาณเอา เราเลยเรียกว่า Floating Point Approximation...