Tutorial

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

By Arnon Puitrakul - 19 กรกฎาคม 2021 - 2 min read min(s)

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 เลยก็ยังได้ รวม ๆ แล้วเราว่ามันทำให้เราทำงานได้เร็ว และ ยืนหยุ่นขึ้นนั่นเอง ทำให้เราชอบ