บทความที่แล้วได้กล่าวถึง for statement ของภาษา Python ซึ่งจะต้องใช้งานควบคู่กับวัตถุประเภท iterable เท่านั้น
บทความนี้จะพูดถึงเทคนิคการจัดการกับลำดับของข้อมูล ( sequence of data ) ที่จะช่วยให้ for statement อ่านเข้าใจง่าย ดูสละสลวยขึ้น และยังมีประสิทธิภาพในการทำงานที่ดีอีกด้วย
เราจะสนใจโจทย์ปัญหาต่อไปนี้เพื่อทำให้เรื่องราวที่จะนำเสนอเป็นรูปธรรมยิ่งขึ้น
ปัญหา ก. การคำนวณหา local maxima ในข้อมูลตารางสองมิติ
ปัญหา
สมมติว่ามีข้อมูลเป็นตารางสองมิติ โดยที่แต่ละช่องมีค่าเป็นจำนวนจริง
นิยาม: ข้อมูลในช่องหนึ่ง ๆ จะจัดว่าเป็น local maximum ถ้าช่องดังกล่าวมีค่ามากกว่าข้อมูลในช่องที่อยู่ติดกันในแนวตั้ง ในแนวนอน หรือในแนวทแยงมุม ( เรียกรวม ๆ ว่าเป็น “ช่องเพื่อนบ้าน” )
จงหาพิกัดของค่า local maximamaxima เป็นคำพหูพจน์ของคำว่า maximum ทุกช่องที่ปรากฏในข้อมูลตารางที่กำหนดให้ ให้ตอบพิกัดซึ่งเป็นค่าดัชนีแนวตั้ง ( row index ) และแนวนอน ( column index ) ของแต่ละช่อง
เพื่อความสะดวกและรวดเร็ว จะขอกำหนดให้ข้อมูลตารางอยู่ในรูปของ
numpy.ndarray
สองมิติ
แต่วิธีการแก้ปัญหาที่จะนำเสนอต่อจากนี้ไป จะไม่จำเพาะเจาะจงกับไลบรารี่ numpy
เพียงอย่างเดียว
ผู้อ่านไม่จำเป็นต้องมีประสบการณ์ในการใช้งานไลบรารี่ดังกล่าวมาก่อนแต่อย่างใด
เราจะยกตัวอย่างข้อมูลตารางสองมิติ ในตัวแปรชื่อ grid
ซึ่งมีค่าดังต่อไปนี้
ข้อมูลตาราง grid
ในตัวอย่างข้างต้น จะมี local maxima อยู่ 3 ช่องซึ่งปรากฏอยู่ที่ตำแหน่ง
( 1, 0 )
, ( 1, 2 )
และ ( 3, 3 )
ตามลำดับ
- ช่องตาราง
grid[ 1, 0 ]
มีค่าเท่ากับ8
จัดว่าเป็น local maximum เพราะมีมูลค่ามากกว่าช่องเพื่อนบ้านทั้งหมดที่อยู่ติดกัน ( อันได้แก่-4
,5
,6
,-3
และ-2
เมื่อพิจารณาช่องเพื่อนบ้านแต่ละช่องเริ่มจากช่องบน วนตามเข็มนาฬิกา ) - ช่องตาราง
grid[ 1, 2 ]
มีค่าเท่ากับ9
จัดว่าเป็น local maximum อีกช่องหนึ่ง เพราะมีมูลค่ามากกว่าช่องเพื่อนบ้านทั้งแปดช่องที่อยู่ติดกัน ( อันได้แก่-2
,0
,1
,2
,5
,-3
,6
และ5
เมื่อพิจารณาช่องเพื่อนบ้านเริ่มจากช่องบน วนตามเข็มนาฬิกา ) - ช่องตาราง
grid[ 3, 3 ]
มีค่าเท่ากับ6
จัดว่าเป็น local maximum เพราะมีมูลค่ามากกว่าช่องที่อยู่ด้านบนทางซ้าย ( ซึ่งมีมูลค่า2
), ช่องซ้าย ( มูลค่า4
) และช่องบนซ้าย ( มูลค่า5
)
ส่วนช่องอื่น ๆ ไม่จัดว่าเป็น local maximum เพราะอาจจะมีช่องเพื่อนบ้านบางช่องที่มีมูลค่ามากกว่าหรือเท่ากับช่องข้อมูลนั้น ๆ
เป้าหมาย: ต่อจากนี้ไปจะขอนำเสนอฟังก์ชันในภาษา Python ที่จะช่วยแก้ปัญหา ก. ข้างต้น โดยเราจะเริ่มพิจารณาจากโค้ดที่ตรงไปตรงมาที่สุด ( แต่อาจจะมีจุดบกพร่องบ้าง ) แล้วเราจะพยายามปรับปรุงโค้ดดังกล่าวทีละนิด จนสุดท้ายกลายเป็นโค้ดที่อ่านง่าย ดูสละสลวย และมีประสิทธิภาพที่ดี
Version 1 – Naïve approach
โค้ดต่อไปนี้นำเสนอการคำนวณ local maxima จากข้อมูลตารางโดยใช้วิธีที่ถึกอย่างตรงไปตรงมาที่สุด ( มีคำอธิบายประกอบอยู่ด้านล่างโค้ด )
ตัวอย่างการเรียกใช้งานฟังก์ชัน local_maxima
( Version 1 ) ข้างต้น
โอ้แม่เจ้า ถึกเสียนี่กะไร! สังเกตว่าเรามี if statement ทั้งสิ้น 8 ชุดเพื่อตรวจสอบว่าค่าของแต่ละช่อง ( i, j )
นั้นมีค่ามากกว่าหรือเท่ากับช่องเพื่อนบ้านที่อยู่ติดกันทั้ง 8 ช่องหรือไม่
( โดยเริ่มต้นเช็คกับช่องเพื่อนบ้านทางมุมบนซ้าย จนไปสิ้นสุดที่ช่องที่มุมล่างขวา )
หากผิดไปจากนี้แปลว่าค่าของช่อง ( i, j )
จะไม่มีทางเป็น local maxima อย่างแน่นอน
ความเลวร้ายยังไม่หยุดแค่นั้น! ในกรณีที่ช่อง ( i, j )
อยู่บนขอบตาราง
จะต้องระมัดระวังไม่นำค่าของช่องดังกล่าวไปเปรียบเทียบกับค่าของช่องที่หลุดขอบตารางออกไป
เช่น ถ้า j == 0
แล้วจะไม่สามารถเปรียบเทียบค่าของช่อง
( i, j )
กับช่อง ( i, j-1 )
ที่อยู่ทางซ้ายมือได้
( จึงเป็นที่มาของการตรวจเช็คเงื่อนไขในรูปของ i > 0
, i < r-1
,
j > 0
และ j < c-1
ในโค้ดข้างต้น )
แม้ว่าโค้ดข้างต้นนี้จะมีลอจิกที่เข้าใจง่าย แต่ก็ไม่ได้หมายความว่าจะเป็นโค้ดที่อ่านให้เข้าใจได้อย่างรวดเร็วทันทีนัก
และยังมีโอกาสเสี่ยงต่อการพิมพ์ผิดเล็ก ๆ น้อย ๆ อีกด้วย ( เอ๊ะ? มีหรือเปล่านะ? )
นอกจากนี้แล้วถ้าวันหนึ่งเราจะต้องเขียนฟังก์ชัน local_minima
ซึ่งทำงานคล้ายกับ local_maxima
แต่ต้องคำนวณหาตำแหน่งของ local minima ทั้งหมดในตารางแทนหละ
เราจะก็อปโค้ดนี้แล้วมาแก้เครื่องหมาย >=
ให้เป็น <=
ทีละบรรทัด ก็ไม่ใช่วิธีที่สวยงามเท่าไหร่นัก
Version 2 – Local 3 × 3 grid approach
จุดสังเกตแรกที่จะช่วยทำให้โค้ดกระชับขึ้นก็คือ เงื่อนไขการเปรียบเทียบ data[ i, j ]
กับช่องเพื่อนบ้านที่อยู่ติดกันมี pattern ที่คล้ายคลึงกัน
( ทุกอสมการอยู่ในรูปของ data[ ... ] >= data[ i, j ]
)
เราสามารถใช้จุดสังเกตดังกล่าวมาเขียนโค้ดให้ง่ายขึ้นได้ดังนี้
การเรียกใช้งานฟังก์ชัน local_maxima
( Version 2 ) ยังคงให้ผลลัพธ์เหมือนเดิม
กล่าวคือเราใช้ ( nbr_i, nbr_j )
แทนตำแหน่งของช่องแต่ละช่องในตารางย่อย 3 × 3
ที่อยู่ในละแวกเพื่อนบ้านของช่องหลัก ( i, j )
โดยใช้ nested for loop ชั้นที่ 3 และ 4 ในโค้ดข้างต้น
ข้อพึงระวัง: เนื่องจากฟังก์ชัน local_maxima
( Version 2 ) ของเราจะพิจารณาตำแหน่งช่อง ( nbr_i, nbr_j )
ทั้งหมด 9 ช่องจากเดิมแค่ 8 ช่อง เราจึงต้องระมัดระวังไม่นำเคสที่ ( nbr_i, nbr_j ) == ( i, j )
มาพิจารณา
Version 3 – Prepare your iterables
แม้ว่าโค้ดใน Version 2 นี้จะปรับปรุงให้กระชับขึ้นมากกว่าโค้ดใน Version 1 แล้ว แต่ก็ทำให้โค้ดบางส่วนอ่านยากขึ้น เพราะมี for statement ซ้อนกันถึง 4 ชั้นเลยทีเดียว
เราจะจัดการกับโค้ดเจ้าปัญหาดังกล่าวให้อ่านง่ายขึ้น
ด้วยการ refactor ลอจิกบางส่วนออกจากฟังก์ชันหลักอย่าง local_maxima
ให้กลายเป็นฟังก์ชันแยกต่างหาก
กล่าวคือเราจะมีฟังก์ชันใหม่อันหนึ่งที่เอาไว้ช่วยไล่เรียงช่อง ( i, j )
ทุกช่องในตาราง
และอีกฟังก์ชันหนึ่งเอาไว้ช่วยไล่เรียงช่องเพื่อนบ้าน ( nbr_i, nbr_j )
ทุกช่องที่อยู่ติดกับช่อง ( i, j )
ที่กำหนดให้
โปรแกรมเวอร์ชันใหม่ของเรามีหน้าตาดังต่อไปนี้
เมื่อเทียบโค้ดของฟังก์ชัน local_maxima
เวอร์ชันใหม่กับเวอร์ชันที่แล้ว ( Version 2 ) มีสิ่งที่เปลี่ยนแปลงไปดังนี้
- ช่อง
( i, j )
แต่ละช่องของตารางจะถูกสร้างโดยฟังก์ชันtable_cells
แทนที่จะเขียน for statement ซ้อนกัน 2 ชั้นขึ้นมาโดยตรง - ช่องเพื่อนบ้าน
( nbr_i, nbr_j )
แต่ละช่องที่อยู่ติดกับช่อง( i, j )
ซึ่งไม่ตกขอบตาราง จะถูกสร้างโดยฟังก์ชันadjacent_cells
แทนที่จะใช้ for statement ซ้อนกันอีก 2 ชั้นผสมกับ if statement เพิ่มเติม
สังเกตว่าโค้ดของฟังก์ชัน local_maxima
( Version 3 ) นี้กระชับและอ่านง่ายขึ้นกว่าเดิม
เราสามารถมองเห็น intention ของฟังก์ชันได้จากโค้ดโดยตรง โดยเราไม่จำเป็นต้องมานั่งตีความอีกต่อไปว่า
for statement ที่ซ้อนกันแต่ละชั้น มีจุดประสงค์เอาไว้ทำงานอะไรบ้าง
นอกจากนั้น ฟังก์ชัน table_cells
และ adjacent_cells
ที่แยกออกมานั้น
สามารถนำไปใช้ประโยชน์ซ้ำในโอกาสอื่น ๆ ได้
( เช่น นำไปใช้เขียนฟังก์ชัน local_minima
เป็นต้น )
เมื่อเราลองใช้งาน local_maxima
( Version 3 ) ก็พบว่าโปรแกรมยังให้ผลลัพธ์เหมือนเดิม
เอาหละ! มาพักหายใจกันแป๊บหนึ่ง เดี๋ยวมาดูกันว่าเราจะทำให้โค้ด Version 3 ข้างต้นให้ดียิ่งขึ้นได้อย่างไรบ้าง
[ insert elevator song here ]
Version 4 Part A: Improving main function
เราจะใช้เครื่องมือบางอย่างที่ Python จัดเตรียมไว้เพื่อทำให้ฟังก์ชัน local_maxima
สั้นลงกว่าเดิม
มาพิจารณาโค้ดเวอร์ชันใหม่นี้กันเลย
เมื่อพิจารณาฟังก์ชัน local_maxima
เวอร์ชันล่าสุดนี้เทียบกับ Version 3
จะพบว่าเราได้ย้าย for statement ชั้นในสำหรับตัวแปร nbr_pos
เข้าไปอยู่ภายใน generator expression ต่อไปนี้
จากนั้น generator expression ดังกล่าวก็ถูกป้อนเป็น input argument ของฟังก์ชัน all
( ซึ่งเป็นฟังก์ชันที่สามารถเรียกใช้งานได้โดยไม่จำเป็นต้อง import
ก่อน )
หากพิจารณา documentation ของฟังก์ชัน all
จะพบว่า input argument สามารถเป็นวัตถุ iterable ใดก็ได้ รวมถึง generator expression ที่เราเขียนไว้ข้างต้นด้วย
สิ่งที่น่าสนใจเกี่ยวกับโค้ด Version 4 a นี้ก็คือ เมื่ออ่านตาม syntax ของโค้ดข้างต้นจะได้ความว่า
“ให้พิจารณาช่อง
pos
แต่ละตำแหน่งในตาราง แล้วหาว่ามีช่องใดบ้างที่มีค่ามากกว่าช่องเพื่อนบ้านnbr_pos
ทุกช่อง”
ซึ่งมีความหมายสอดคล้องและคล้ายคลึงกับนิยามของโจทย์ที่กำหนดไว้ในช่วงต้นของบทความนี้อย่างมาก
Version 4 Part B. Using generator expression with table_cells
ในฟังก์ชัน local_maxima
( Version 3 ) สังเกตว่าเราเรียกใช้ฟังก์ชัน table_cells
( Version 3 )
ซึ่งให้ผลลัพธ์ออกมาเป็น list
ของตำแหน่งทุกช่องในตาราง
แล้วจากนั้นเราจึงไล่ดูช่องตารางทีละช่องโดยจาก list ดังกล่าวโดยใช้ for statement
แต่จากบทความที่แล้วเราได้กล่าวไว้ว่า
วัตถุที่ใช้คู่กับ for statement สามารถเป็นวัตถุ iterable ใดก็ได้ ไม่จำเป็นต้องเป็นโครงสร้างข้อมูลอย่างเช่น list เสมอไป
ฉะนั้นแล้วเราจะลองแก้ไขฟังก์ชัน table_cells
ให้รีเทิร์นค่าออกมาเป็นวัตถุ iterator
โดยเปลี่ยนจาก list comprehension ให้กลายเป็น generator expression ซึ่งออกมาเป็นโค้ดดังต่อไปนี้
แล้วการเขียนฟังก์ชันที่รีเทิร์นค่าเป็นวัตถุ iterator มันดีกว่าการสร้าง list โดยตรงอย่างไรบ้างในสถานการณ์เช่นนี้?
-
ฟังก์ชันที่สร้างวัตถุ iterator นั้นให้อิสระแก่ผู้เรียกใช้งานฟังก์ชันมากกว่าการสร้าง list นั่นเพราะว่าเราสามารถแปลงวัตถุ iterator ดังกล่าวให้กลายเป็น list ได้ด้วยการแคสด้วย
list
constructor ได้เสมอ ( list
จะแปลงข้อมูล iterable ใด ๆ ที่รับมาให้กลายเป็น list ตามที่ระบุไว้ใน documentation )ยกตัวอย่างเช่น
-
เมื่อพิจารณาในกรณีที่โปรแกรมเราใช้งาน for statement กับวัตถุ iterable เพื่อประมวลผลสมาชิกเพียงครั้งละ 1 ตัวเท่านั้น ( ดังเช่นที่เกิดขึ้นภายในฟังก์ชัน
local_maxima
Version 3 ซึ่งเราจะพิจารณาช่องตารางเพียงทีละ 1 ช่องเท่านั้น )หมายความว่าเราไม่จำเป็นต้องคำนวณตำแหน่งของช่องตารางทุกช่องไว้ล่วงหน้าทั้งหมด ซึ่งยังมีผลให้เปลืองพื้นที่หน่วยความจำเพื่อเก็บลำดับของช่องตารางทั้งหมดดังกล่าวอีกด้วย ( ลองคิดดูว่าหากข้อมูลตารางมีขนาดใหญ่เช่น เราจะเสียพื้นที่ไปกี่ MB โดยไม่มีเหตุจำเป็น )
ฉะนั้นแล้วเราเลือกที่จะไม่สร้างลำดับของช่องตารางทั้งหมดไว้ล่วงหน้า แต่จะค่อย ๆ สร้างลำดับดังกล่าวอย่างขี้เกียจที่สุด ( เรียกว่าเป็น lazily constructed sequence ) กล่าวคือเราจะคำนวณสมาชิกตัวถัดไปของลำดับนี้ก็ต่อเมื่อถึงเวลาที่ for statement ขอเรียกดูสมาชิกตัวดังกล่าวไปประมวลผลเท่านั้น ซึ่งการเขียน generator expression ก็เป็นหนึ่งวิธีที่จะสร้าง lazily constructed sequence อย่างที่ปรารถนาได้
บทแทรก
ข้อตกลง ( convention ) ของภาษา Python กำหนดไว้ว่า
วัตถุประเภท iterator ถือว่าเป็น iterable ชนิดหนึ่งที่สามารถไล่เรียกดูสมาชิกทุกตัวได้เพียงรอบเดียวเท่านั้น
( เพราะในทางเทคนิคแล้ว method ชื่อ __iter__( self )
มักจะรีเทิร์น self
ออกมาโดยตรง
ทำให้ไม่เกิด iterator อันใหม่สำหรับวัตถุที่เป็น iterator อยู่แล้วแต่แรก )
ผู้ใช้งานจึงควรระมัดระวังการใช้งานวัตถุ iterator ซ้ำกันเกิน 1 รอบ ยกตัวอย่างเช่น
หมายเหตุ: อันที่จริงแล้ว เราอาจเรียกใช้ฟังก์ชัน
numpy.ndindex( *shape )
แทนการสร้างและเรียกใช้งานฟังก์ชัน table_cells( shape )
ก็ได้
Version 4 Part C. Using generator function for adjacent_cells
เมื่อสักครู่เราแก้ไขฟังก์ชัน table_cells
จากเดิมที่รีเทิร์นค่าเป็น list ให้กลายเป็นวัตถุ iterator ใน Version 4 b กันไปแล้ว
เราจะนำไอเดียเดียวกันนี้มาประยุกต์ใช้กับฟังก์ชัน adjacent_cells
ดูบ้าง
แต่ถ้าจะเปลี่ยนโค้ดของ adjacent_cells
ให้หันมาใช้ generator expression ก็คงยุ่งยากวุ่นวายเกินไป
ดังนั้นเราจะใช้ความสามารถอีกอย่างหนึ่งของภาษา Python นั่นก็คือ generator function
เราจะข้ามไปดูโค้ดเวอร์ชันใหม่ของ adjacent_cells
กันเลย
สิ่งที่เปลี่ยนไปจากโค้ดเดิมคือ
- เราลบโค้ดส่วนที่เกี่ยวข้องกับลิสต์
adjacent_indices
ออก และ - แทนที่เราจะ append ค่าของ
( nbr_i, nbr_j )
ยัดลงใน list เราเลือกใช้yield
statement เพื่อคืนค่าตำแหน่งเหล่านั้นทีละตัวแทน
“แล้วคำสั่ง yield
มันทำอะไร?” อธิบายโดยคร่าวคือ
- เมื่อฟังก์ชันใดก็ตามมี keyword คำว่า
yield
ปรากฏอยู่ จะทำให้ฟังก์ชันดังกล่าวนั้นกลายร่างเป็น “generator function” ในทันที - การเรียกใช้งาน generator function จะให้ผลลัพธ์เป็น “generator object”
ซึ่งจัดเป็นวัตถุ iterator ชนิดหนึ่ง
แต่ว่า body ของฟังก์ชันดังกล่าวจะยังไม่เริ่มทำงานในทันที
อันที่จริงแล้วเมื่อเรารัน generator expression
( ดังเช่นในฟังก์ชัน
table_cell
Version 4–B ) ก็จะได้ผลลัพธ์เป็น generator object เช่นเดียวกัน - เมื่อมีการเริ่มไล่เรียกดูสมาชิกแต่ละตัวของวัตถุ generator/iterator ( ไม่ว่าจะเกิดจากการใช้งานวัตถุดังกล่าวกับ for statement หรือด้วยกระบวนการอื่นใดก็ตาม ) แล้ว generator function ดังกล่าวจึงจะเริ่มทำงาน
- และทุก ๆ ครั้งที่ generator function ทำงานถึงบรรทัดคำสั่ง
yield
ค่าของ นิพจน์ ใน yield statement จะถูกรีเทิร์นออกไปเป็นหนึ่งค่าของวัตถุ generator/iterator ส่วน generator function นี้จะหยุดการทำงานชั่วคราว จนกว่าจะมีการเรียกขอดูค่าถัดไปของวัตถุ generator/iterator นี้ - วัตถุ generator/iterator จะสิ้นสุดลงก็ต่อเมื่อเกิดจากรีเทิร์นออกจากฟังก์ชัน ( ไม่ว่าจะด้วย return statement หรือว่าฟังก์ชันถูกรันจนจบถึงบรรทัดสุดท้ายก็ตาม )
ดังนั้นแล้ว ฟังก์ชัน adjacent_cells
( Version 4 c ) นี้ก็จะสร้างวัตถุ iterator
ซึ่งจะไล่เรียงช่องเพื่อนบ้านทุกอย่างที่อยู่ติดกับช่อง main_pos
ที่รับเข้ามาเป็นข้อมูลนำเข้า
ออกมาทีละช่องผ่านคำสั่ง yield
นับว่าเป็น lazily constructed sequence เช่นเดียวกับฟังก์ชัน table_cells
( Version 4 b )
Summary of Version 4
เรานำโค้ด Version 4 ของทั้งสามฟังก์ชันมาประกอบรวมกัน กลายเป็นโปรแกรมดังต่อไปนี้
[ insert bogo advert here ]
ช่วงโปรโมชันพิเศษ เราขอแถมโจทย์ปัญหาอีก 1 ข้อ
ปัญหา ข. ตารางสองมิติมี local maximum อย่างน้อยหนึ่งช่องหรือไม่
ปัญหา
สมมติว่ามีข้อมูลเป็นตารางสองมิติ โดยที่แต่ละช่องมีค่าเป็นจำนวนจริง
จงหาว่าในข้อมูลตารางนี้ มีช่องอย่างน้อน 1 ช่องที่มีค่าเป็น local maximum หรือไม่
โดยให้ตอบเป็นค่าบูลีน True
หรือ False
หมายเหตุ: โปรดสังเกตว่า มีกรณีที่คำตอบของคำถามข้างนี้สามารถเป็น False
เช่น ในกรณีที่ตารางมีขนาดใหญ่กว่า 1 × 1 และทุกช่องในตารางมีค่าเท่ากัน
สังเกตว่าปัญหา ข. นี้ถามหา “existence of a solution” ซึ่งเป็นเพียง corollary ของปัญหา ก. ดั้งเดิมที่ให้ “enumerate all solutions” เท่านั้นเอง
สมมติว่ามีข้อมูลตารางอยู่ในตัวแปรชื่อ table
แล้วต้องการค้นหาว่า table
มี local maximum อย่างน้อยหนึ่งช่องหรือไม่
เราสามารถแก้ปัญหาง่าย ๆ ได้ดังนี้
- สั่งรัน
local_maxima( table )
เพื่อคำนวณหาลิสต์ของ local maxima ทั้งหมดของข้อมูลในตัวแปรtable
- ตรวจสอบว่าผลลัพธ์จากข้อที่แล้ว เป็นสิลต์ที่มีความยาวอย่างน้อย 1 หรือไม่
แต่ประสิทธิภาพของวิธีการแก้ปัญหาข้างต้นนี้ยังไม่เป็นที่น่าพอใจ
กล่าวคือโจทย์ข้อนี้เราต้องการทราบแค่ว่า “มีคำตอบอย่างน้อย 1 ช่องหรือไม่” เพียงแค่นั้น
แต่ฟังก์ชัน local_maxima
ที่เราเรียกใช้งานกลับรีเทิร์นค่าเป็น “ลิสต์ของคำตอบทั้งหมด”
ซึ่งเป็นข้อมูลที่ มากเกินความจำเป็น ในการตอบคำถาม
จะดีหรือไม่ หากเราสามารถควบคุมการทำงานฟังก์ชัน local_maxima
สร้างลำดับของช่องตารางที่เป็น local maximum
ที่มีลักษณะเป็น lazily constructed sequence เช่นเดียวกับฟังก์ชัน table_cells
และ adjacent_cells
( หมายความว่า ช่องตารางที่มีค่าเป็น local maximum แต่ละช่องจะถูกคำนวณก็ต่อเมื่อถึงเวลาที่ช่องดังกล่าวถูกเรียกดูเท่านั้น )
Version 5 – Changing return type of main function
ก่อนที่จะพูดถึงจุดประสงค์ในการดัดแปลงฟังก์ชัน local_maxima
ให้รีเทิร์นค่าวัตถุ iterator แทนที่จะเป็น list
แบบเก่า
เราจะไปดูโค้ดที่มีการแก้ไขกันก่อน
เราดัดแปลงให้ฟังก์ชัน local_maxima
กลายเป็น generator function ใน Version 5 นี้
( ด้วยกระบวนการที่คล้ายกับการดัดแปลงฟังก์ชัน adjacent_cells
ใน Version 4 c )
หมายเหตุ: อย่าลืมว่าถ้าเราต้องการผลลัพธ์เป็นลิสต์แบบเก่า เราก็เพียงแค่นำวัตถุ iterator ไปใส่ใน list constructor ได้ดังที่เคยกล่าวไปแล้ว
แล้วการทำให้ฟังก์ชัน local_maxima
รีเทิร์นเป็นวัตถุ iterator จะช่วยให้ตอบปัญหา ข. อย่างมีประสิทธิภาพได้อย่างไร?
ลองพิจารณาจิ๊กซอว์สองชิ้นนี้
- เราต้องการทราบเพียงแค่ว่าตารางข้อมูลสองมิติที่กำหนดให้ มี local maximum อย่างน้อย 1 ช่องหรือไม่ นั่นแปลว่าหากเราพบ local maximum ช่องแรกของตารางแล้ว ค่า local maximum ที่เหลือในตารางก็ไม่มีนัยยะสำคัญอีกต่อไปนี้
- ฟังก์ชัน
local_maxima
( Version 5 ) เป็น generator function ที่จะค้นหาตำแหน่งของ local maximum ในข้อมูลตารางที่รับเป็นข้อมูลนำเข้า เพียงทีละ 1 ช่องเท่านั้น และจะมีหยุดการทำงานชั่วคราวจนกว่าจะมีการขอให้ดำเนินการค้นหา local maximum ตัวถัดไป
ดังนั้นแล้ว เราขอตั้งปัญหาในรูปทั่วไปดังต่อไปนี้
ปัญหาย่อย: ตรวจสอบว่าวัตถุ iterable ไม่ว่างเปล่า
ปัญหา
เราจะสามารถตรวจสอบว่าวัตถุ iterable ใด ๆ มีสมาชิกอย่างน้อย 1 ตัวหรือไม่ ได้อย่างไร โดยไม่จำเป็นต้องพยายามไล่ดูสมาชิกของวัตถุ iterable เกิน 1 ตัว
เราสามารถเขียน รูปแบบนิพจน์ any( True for _ in iterable )
เพื่อทดสอบว่าวัตถุ iterable
ในนิพจน์ดังกล่าวมีสมาชิกอย่างน้อย 1 ตัวหรือไม่ได้
รูปแบบนิพจน์ดังกล่าวอาศัยความสามารถ short-circuiting ของฟังก์ชัน any
เพื่อยุติการทำงานเมื่อทราบผลลัพธ์เป็นที่แน่นอนแล้ว
ยกตัวอย่างเช่น
มีคำถาม 2 คำถามที่อาจเป็นที่สนใจ แต่จะขออนุญาตทิ้งไว้ให้เป็นคำถามชวนคิดด้วยตนเอง
- ทำไมเราจึงไม่ใช้รูปแบบนิพจน์
any( iterable )
ไปเลย แต่กลับใช้รูปแบบนิพจน์any( True for _ in iterable )
แทน? - เราจะออกแบบการทดลองอย่างไร จึงจะทำให้มั่นใจได้ว่ารูปแบบนิพจน์
any( True for _ in iterable )
จะยุติการทำงานทันทีหลังจากที่วัตถุ iterable ปล่อยค่าสมาชิกตัวแรกออกมาแล้ว?
มาพิจารณาปัญหา ข. กันอีกครั้งหนึ่ง นั่นคือเราอยากทราบว่าข้อมูลตารางที่กำหนดให้มี local maximum อย่างน้อย 1 ช่องหรือไม่? แต่ในคราวนี้ เรามีเครื่องมือพร้อมที่จะแก้ปัญหานี้อย่างมีประสิทธิภาพมากขึ้นกัน
โค้ดที่จะใช้ตรวจสอบว่า “ผลลัพธ์ของ local_maxima
( Version 5 ) จะเป็นวัตถุ iterator ที่ไม่ว่างเปล่าหรือไม่”
อย่างมีประสิทธิภาพ สามารถเขียนได้ดังนี้
บทสรุปส่งท้าย
บทความนี้ได้นำเสนอวิธีการต่าง ๆ ที่จะช่วยทำให้การทำงานกับ for statement และวัตถุ iterable จนกลายออกมาเป็นโค้ดที่อ่านง่าย ดูสละสลวย และมีประสิทธิภาพที่ดี
เมื่อใดก็ตามที่เราจะต้องทำงานกับ for statement หรือวัตถุ iterable เราอาจจะถามคำถามต่อไปนี้กับตนเอง
- มีโค้ดส่วนใดที่เกี่ยวข้องกับ iteration ที่สามารถแยกออกเป็นฟังก์ชันต่างหากที่รีเทิร์นค่าออกมาเป็นวัตถุ iterator หรือไม่?
- มีโค้ดส่วนใดที่เดิมรีเทิร์นค่าเป็น list แล้วจะมีโอกาสให้โค้ดมีประสิทธิภาพดีขึ้นหากเราเปลี่ยนให้รีเทิร์นเป็นวัตถุ iterator หรือไม่?
- มีฟังก์ชันใน Python standard library ใดที่จะช่วยให้การทำงานกับวัตถุ iterable ง่ายขึ้นหรือไม่? ( คำแนะนำ: สามารถค้นหาฟังก์ชันเหล่านี้เพิ่มเติมได้จากแพ็กเกจ itertools และ more-itertools เป็นต้น )
และขอปิดท้ายด้วยโค้ดเวอร์ชันสุดท้ายของฟังก์ชันนี่เราได้พยายามปรับปรุงให้ดียิ่งขึ้น
ป.ล. บทความนี้อาจจะยาวกว่าปกติ ต้องขออภัยมา ณ ที่นี้ ขอขอบคุณเพื่อน ๆ ที่เสียสละเวลาอ่าน draft ของบทความและให้ feedback เพื่อใช้ปรับปรุงบทความนี้ให้ดียิ่งขึ้น และขอขอบคุณผู้อ่านทุกอ่านที่อ่านบทความจนจบครับ
ในบทความถัด ๆ ไปจะขอเว้นจากการเขียนเกี่ยวกับ Python Iterables สักพักหนี่ง แต่อาจจะพูดถึงหัวข้อที่ advance มากขึ้น หรืออาจจะพูดถึงเรื่องบันเทิงอื่น ๆ บ้างตามประสา โปรดติดตามชม