Tutorial

รู้จักกับ Context Manager ใน Python

By Arnon Puitrakul - 10 กุมภาพันธ์ 2022 - 1 min read min(s)

รู้จักกับ Context Manager ใน Python

หลาย ๆ ครั้งเวลาเราทำงานกับพวก File หรือ Resource ต่าง ๆ ที่มันต้องมีการจอง เราจะต้องมีหลาย ๆ ขั้นตอนกว่าเราจะทำงานกับมันได้ โดยที่ใน Python เองมันก็มีเครื่องมือที่จะเข้ามาช่วยทำให้เราทำงานได้เร็วขึ้น เขียน Code ได้สะอาดขึ้นเยอะเลย นั่นคือ Context Manager วันนี้เราจะมาดูกันว่า มันคืออะไร มันทำงานอย่างไร และ เราจะใช้งานมันได้อย่างไร

ปกติเราทำงานอย่างไร

ปกติแล้ว เวลาเราทำงานกับพวก Resource ภายนอกต่าง ๆ เช่น File, Network และ Database มันก็จะมีขั้นตอนของมันอยู่หลายขั้นตอนเลย กว่าเราจะทำงานกับมันได้ ตัวอย่างเช่น File

input_file = open('data.dat', 'r')

while (line := input_file.readline()) != '' :
    # process here
    
input_file.close()

Code ด้านบนจะเป็นการที่เราจะพยายามอ่านไฟล์ แล้วอาจจะเอาไปทำอะไรบางอย่างต่อไป ถือว่าเป็น Process ที่เราน่าจะคุ้นเคยกันดีเลยละ เราจะเห็นว่า ก่อนที่เราจะถึงขั้นตอนของการอ่านไฟล์ เราจะต้องผ่าน Process แรกก่อน นั่นคือขั้นตอนของการเปิดไฟล์ จากนั้น ก็อ่าน สุดท้าย ก่อนที่เราจะจบ เราก็จะต้องทำการปิดไฟล์ก่อน

ถามว่า แล้วทำไม เราต้องทำแบบนั้นด้วยละ เราเปิดแล้วไม่ปิดได้มั้ย คำตอบคือ เครื่องมันไม่ได้ฉลาดขนาดนั้นเนอะ เหมือนไฟบ้านเลย เปิด แล้วก็ต้องปิด เพราะว่า ถ้าเราเปิดไว้ มันอาจจะเป็นการ Lock ไม่ให้คนอื่น หรือในที่นี้ก็คือ Process อื่น ๆ เข้ามาใช้งานได้ ทำให้รวนไปหมดได้ทั้งระบบที่เข้ามาใช้ไฟล์นี้เลยก็ว่าได้

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

เอาหล่ะ Code ด้านบน มันจะมีอะไรมั้ย ที่ทำให้ มันจะเปิด แล้ว อ่านแล้ว แต่ปิดไม่ได้ ถ้าง่าย ๆ ก็คือ อาจจะมี Error อะไรบางอย่างระหว่างทางเช่น เราอาจจะ Parse ข้อมูลผิด หรือเขียน Code ผิดอะไรก็ว่ากันไป ทำให้มัน Error แล้วเด้ง ออกมาเลย ทำให้ Code ในบรรทัดที่มันจะปิดไฟล์ มันก็จะไม่ได้ทำงาน นั่นแปลว่า OS ก็จะเข้าใจว่า เห้ย มันต้อง Lock ไม่ให้คนอื่นเข้านะ ไม่งั้น คบซ้อน สับรางไม่ทันเด้อ งั้น เราจะแก้ปัญหานี้อย่างไรดี

try :
    input_file = open('data.dat', 'r')
    while (line := input_file.readline()) != '' :
        # process here
except Exception as e :
    print(f"Error {e}")
finally :
    input_file.close()

งั้นในเมื่อ ถ้าเรากลัว Error เราก็ Try...Except เลยสิ เพราะยังไง Finally มันจะทำงานเสมอ ไม่ว่า Expressions บน Try จะทำสำเร็จหรือไม่ก็ตาม มันก็จะถูกทำงานหลังสุดเสมอ ทำให้เรามั่นใจได้ว่า เออ เราปิดไฟล์แล้วแน่ ๆ นั่นเอง

ถามว่า แล้วการทำแบบนี้ มันทำได้มั้ย คำตอบคือ ทำได้นะ แต่เราดู Code สิ มันดูไม่ค่อยโอเคเท่าไหร่เลยใช่มั้ย ไหนจะเรื่องที่เราเขียน Exception แบบนี้อีก ไม่ Healthy เท่าไหร่ เพราะอาจจะโดน Programmer คนอื่นโยกหน้าเข้าได้ เขียนอะไรของเ_งเนี่ย ! หรือกระทั่งเคสที่เราอาจจะลืมเขียนปิดเลยก็ได้เหมือนกันนะ อันนี้บอกเลยว่า น่าโยกหน้าสักทีมาก

Context Manager เข้ามาช่วยแล้ว

with open('data.dat', 'r') as input_file :
    while (line := input_file.readline()) != '' :
        # process line

เครื่องมือที่ Python เตรียมมาให้เราคือ การใช้ with เข้ามาช่วย เราลองดูจาก Code ด้านบนกันก่อน อย่างแรกที่เราจะสังเกตเลยว่า มันสั้นกว่าเดิมเยอะมาก แค่จำนวนบรรทัดก็ต่างจาก Code ก่อนหน้าแล้ว แต่เราจะบอกเลยว่า Code สั้น ๆ นี้ก็การันตีว่า มันจะทำการปิดไฟล์ ไม่ว่าจะเกิดอะไรขึ้น ได้เหมือนกับ Code ในชุดก่อนหน้าเลย

with open('data1.dat', 'r') as input_file_1, open('data2.dat', 'r') as input_file_2 :
    while (line := input_file_1.readline()) != '' :
        # process line

นอกจากนั้น ความเปรี้ยวเยี่ยวราดอีกอันก็คือใน Python 3.1 เขาอนุญาติให้เราสามารถใส่หลาย ๆ Expression ใน with ได้ด้วย โดยการคั่นด้วยเครื่องหมาย Comma เช่น Code ด้านรบนเป็นต้น

การทำงานของมันจะประกอบด้วย Method จำนวน 2 ตัวด้วยกันคือ __enter__ และ __exit__ โดยที่เมื่อ with ถูกเรียก มันจะไปเรียกคำสั่ง enter ที่เรา Implement ไว้ หรือ Python เขาทำมาให้เราแล้ว จากนั้น เราก็ทำงานอะไรของเราไป และสุดท้าย เมื่อมันจะออกจาก Code Block ภายใน with ไม่ว่าจะออกแบบยังมีใจกันอยู่ หรือบาดหมาง เกิด Error มันก็จะไปทำการเรียก exit ให้เราเองเลย ทำให้ Code ด้านบน กับตัวก่อนหน้ามีความคล้าย ๆ กันอยู่คือ ไม่ว่ายังไง เราก็จะได้ทำการปิดไฟล์

แต่เอ๊ะ แล้วตรงไหนที่บอกว่า ปิดไฟล์ เราจะบอกว่า มันไม่มีในนี้ เพราะ Python จัดการมาให้เราแล้ว ใน Method __exit__ ที่มันมีการสั่งให้ปิดไฟล์แล้ว นั่นทำให้ เราไม่จำเป็นต้องมาสั่งเองแล้ว มันจัดการให้เราเองเลย เมื่อโปรแกรมมันจะออกจาก Block ของ with ตรงนี้

ดังนั้น ใน __enter__ มันจะเป็น Method สำหรับการเชื่อมต่อกับ Resource ต่าง ๆ ไว้ได้เลย เช่นถ้าเป็น File เราก็สามารถเรียกเปิดไฟล์จริง ๆ ได้เลย แล้วอาจจะคืนค่ากลับมาเป็นพวก File Cursor อะไรก็ว่ากันไป และสุดท้าย เมื่อเราทำงานกับมันเสร็จ มันจะออกจาก Block มันก็จะไปเรียก __exit__ ที่มันจะมี Logic ในการ Teardown Connection ต่าง ๆ ระหว่างไฟล์นั้น ๆ กับโปรแกรมของเรา

สมมุติว่า ถ้าเราต้องการโปรแกรมที่จะคุย API กับฝั่ง Server แล้วเปิด Connection ไว้เรื่อย ๆ จนกว่าเราจะเสร็​จ เราก็อาจจะเขียน Method __enter__ ให้มันเปิด หรือเชื่อมต่อกับ API Server ไว้ได้เลย แล้วอาจจะคืนค่ากลับมาเป็น Session เพื่อให้เราสามารถเอาไปใช้งานต่อได้ แล้วสุดท้าย เมื่อเราใช้งานเสร็จ ก็จะวิ่งไปที่ __exit__ เราก็อาจจะเขียนเป็น Logic สำหรับการตัดการเชื่อมต่อกับ Server เพื่อให้ผู้ใช้คนอื่น ๆ เข้ามาใช้งานต่อได้

ข้อเสียมันคืออะไร ?

ในกรณีการใช้งานทั่ว ๆ ไป เรามองว่า การใช้ with มันดีมาก ๆ นะ มันลดความผิดพลาดที่อาจจะเกิดขึ้นจากการลืมปิด หรือคืน Resource ต่าง ๆ ได้เป็นอย่างดีเลย แต่ ๆ มันจะมีเคสที่เรามองว่า การใช้ with ตรง ๆ เลยอาจจะยังไม่ตอบโจทย์ 100% เช่นเคสที่เราจะต้อง Custom Logic สำหรับการ enter และ exit ของ Session นั้น ๆ เช่น เราอาจจะต้องมีการเขียนอะไรลงไปในไฟล์ ก่อนที่จะปิด อาจจะเป็นพวก Sealing Latter ต่าง ๆ ก็ว่ากันไป

ถ้าเราใช้ with ตรง ๆ เลย มันก็จะทำได้แค่ปิดเท่านั้น อาจจะมีเคสที่ Sealing Latter มันจะไม่ถูกเขียนลงไปด้วย อาจจะทำให้ เราอ่านมันไม่ได้อีกเลยก็เป็นได้ วิธีแก้ก็จะมีอยู่ 2 วิธีด้วยกันคือ การใช้ Try...Except เหมือนเดิมก็ได้ แล้วเราอาจะ Refactor มันไปอยู่อีก Function ก็ได้ หรือ การใช้ Context Manager ซ้อนเข้าไปอีกที อาจจะเขียนเป็น Class สำหรับการเขียนข้อมูลลงอะไรแบบนั้นก็ได้ แล้วใน Exit เราก็เขียน Logic สำหรับเขียนด้วย Latter Sealing เข้าไปด้วย เพื่อให้เรามั่นใจได้ว่า ไม่ว่าอะไรจะเกิดขึ้น Latter Sealing มันจะถูกเขียนเสมอนั่นเอง ก็ทำได้เหมือนกัน ขึ้นกับการตกลง และ การเขียนของเราเลยว่าจะเลือกวิธีแบบในการจัดการกับปัญหา

ส่วนตัวเรา เราก็จะใช้ Context Manager อยู่บ่อย ๆ เลยนะ เพราะมันทำให้ Code ของเราสะอาดขึ้นเยอะมาก ๆ กับเคสที่ซับซ้อนส่วนใหญ่ เราก็จะใช้วิธีการเขียน Context ซ้อนเข้าไปอีกชั้นเลย ก็ทำให้เอาจริง ๆ เราว่าคนที่ไม่คุ้น ก็อาจจะมี งง ในครั้งแรก ได้

สรุป

การใช้ Context Manager ทำให้การทำงานของเราง่ายกว่าเดิมมาก ๆ โดยเฉพาะการที่เราจะต้องเรียกใช้งานพวก Resource ด้านนอกทั้งหลายไม่ว่าจะเป็น File, Network และ Database ต่าง ๆ เรียกได้ว่า เป็นการลดความผิดพลาดที่อาจจะเกิดขึ้นได้เยอะมาก ๆ ในการเขียน Code เพราะมันจะทำการเรียกส่วนของการ Teardown ต่าง ๆ ให้เราเองเลย เมื่อมันออกจาก Code Block ของ with เอง เพราะบางครั้ง เราก็ต้องยอมรับว่า มันก็มีลืมเขียนกันบ้าง และ ที่สำคัญเลย มันยังทำให้ Code ของเรานั้นดูเรียบร้อยมากกว่าการเขียน Try...Except แน่ ๆ ลองไปใช้งานกันดูได้ มันดีย์ จริง ๆ