การจัดการ Python Dependencies ยังไงให้ เราชนะ !

เมื่อหลายวันก่อน มีคนถามเข้ามา เป็นคำถามที่น่าสนใจมาก ๆ คือ เรื่องของการจัดการ Dependencies ต่าง ๆ บน Python เพราะต้องยอมรับว่า เราก็ไม่ค่อยเขียน Python เพียว ๆ เท่าไหร่ เราก็จะมีการ Install Module หรือพวก Package ต่าง ๆ เข้ามา ไม่ว่าจะผ่าน pip เอย หรือ conda อะไรเลย แต่คำถามคือ แล้วเราจะแชร์พวก Configuration ต่าง ๆ ไปให้อีกเครื่องได้อย่างไร ทำแบบไหนมันมีข้อดีข้อเสียอย่างไรบ้าง

การแยก Environment

โดยทั่วไปแล้ว เวลาเราทำงานบน Project ใหญ่ ๆ จริง ๆ เราจะแนะนำไม่ให้เราติดตั้งพวก Package ในเครื่องตรง ๆ เลย แนะนำให้แยกเป็น Environment ของแต่ละ Project ไปเลย ผ่านโปรแกรมอะไรก็แล้วแต่เราเลย อาจจะใช้ virtualenv หรือตัวที่เราใช้งานอยู่ก็คือ conda

ถามว่า ทำไมเราต้องแยก Environment เป็นเพราะบางครั้งเวลาเราทำงานหลาย ๆ งานในเครื่องเดียวกัน เช่นเราบอกว่า ในเครื่องเราจะต้องทำงานกับ 2 Project โดยที่เราจะต้องใช้ Python คนละ Version กันเช่น Project A ใช้ Python 3.10 แต่ในขณะที่ Project B อาจจะต้องใช้ได้แค่ Python 3.9 เท่านั้น เพราะ Tensorflow ไม่รองรับ แค่นี้ก็ก่อปัญหาได้พอสมควรแล้ว

ยังไม่นับพวก Dependencies ของ Package แต่ละตัวอีกว่า ถ้าเราลงต่าง Version กันมันก็จะต้องการ Version ที่ไม่เท่ากันอีก ก็คือ แค่ 2 Project ก็สามารถก่อร่างความชิบหายในการทำงานได้มหาศาลมาก ๆ แล้ว ดังนั้น เราแนะนำว่า เวลาเราทำงานจริง ๆ เราควรแยก Environment

โดยที่หลักการของมันจริง ๆ ก็คือแต่ละ Environment มันแยก Python Execution และ Dependencies ออกจากกันจริง ๆ เลย คือเหมือนเราลง Python หลาย ๆ ตัวในเครื่องเดียวกันเลย ทำให้เราสามารถที่จะแยก Version ลงได้แบบอิสระเลย ถ้าเราลองใช้คำสั่ง which ออกมาดู เราจะเห็นเลยว่า Execution มันก็อยู่คนละที่เลย

อย่างใน conda เองมันจะมี base Environment ที่เป็นตัวเริ่มต้น ตัวหลักเลย ถ้าเราไม่ Activate ตัวไหนมันจะเอา base ขึ้นมา เราก็จะลงพวก Package ที่เราใช้งานบ่อย ๆ เช่น pandas, matplotlib, joblib, numpy, scipy อะไรพวกนั้นทิ้งไว้เลย เพราะเรามั่นใจว่า พวกนั้น เราใช้งานบ่อย ๆ อยู่ ๆ อยากจะทำขึ้นมา เราจะได้ไม่ต้องมาลงอะไรใหม่

Basic Environment Control with Conda

สำหรับคนที่ยังไม่คุ้นเคยกับ conda แนะนำให้ลองใช้ดูได้ มันโอเคเลยละ เราจะมาเล่า Command Line คร่าว ๆ ที่ใช้ในการจัดการพวก Environment ใน Conda กัน

หลัก ๆ แล้วเวลาเราทำงานกับ Environment เราจะทำงานกับคำสั่ง conda env แล้วตามด้วยคำสั่งของมัน เช่น conda env list คือ ให้มันทำการ List Python Environment ของ Conda ในเครื่องเราออกมา ซึ่งเราจะเห็นเลยว่า Path ของแต่ละ Environment มันจะไม่เหมือนกัน

อีกคำสั่งที่เราใช้งานกันบ่อย ๆ และเป็นจุดเริ่มต้นคำสั่งแรกในการทำงานกับ Environment เลยคือ เราจะต้องสร้าง Environment ผ่านคำสั่ง conda env create แล้วเราจะใช้ Argument -n เพื่อกำหนดชื่อของ Environment เช่นเราตั้งชื่อว่า main-web ก็ใส่เข้าไป และ เรายังสามารถระบุพวก Python Version ได้ด้วย ผ่าน Argument python= แล้วตามด้วยเลข Version เช่น python=3.10 ก็คือ เราจะให้มันโหลด Python Version 3.10 เข้ามา หรือ เราจะใช้ Python อื่น ๆ ก็ได้เช่น BioPython

หลังจากนั้น การที่เราจะใช้งานได้ เราจะต้องทำการเปิดใช้งาน Environment ที่เราพึ่งสร้างขึ้นมาก่อน ผ่านคำสั่ง conda activate แล้วตามด้วยชื่อของ Environment จากในรูป จะเห็นว่า ก่อนที่เราจะ activate ด้านหน้าของ Shell เราจะมีวงเล็บด้านหน้าว่า base คือ เรากำลังอยู่ใน base Environment อยู่ แต่หลังจาก เรา activate main-web ออกมา base ที่อยู่ด้านหน้าก็หายไป กลายเป็น main-web แทน

เวลาเราใช้งาน Environment เราก็สามารถลง Package อะไรได้ตามปกติเลย ไม่ต้องกลัวทับกับ Environment อื่น ๆ เหมือนกับเราเริ่มต้นชีวิตใหม่ ต้องลง Package ที่เราต้องใช้ใหม่ทั้งหมด ย้ำกว่า ทั้งหมดนะ คือ มันจะมาเป็น Python เปล่า ๆ ที่ไม่มี Package ที่เราใช้งานเลย จากในรูปถึงในเครื่องเรา บน base Environment เราจะลง pandas ไปแล้ว แต่พอเราสลับมาใช้อีก Environment เราก็ต้องลงใหม่เด้อ

และสุดท้าย เราทำงานเสร็จแล้ว เราจะลบ Environment ทิ้ง เราก็อาจจะไป Activate Environment อื่น ๆ ก่อน อาจจะไป base แล้วก็รันคำสั่ง conda env remove -n main-web ก็คือ เราจะต้องใส่ Argument -n เพื่อบอกว่า เราจะลบ Environment ชื่ออะไรนั่นเอง

นอกจากนั้น อาจจะลองไปดูเพิ่มได้ เราสามารถ Duplicate Environment ที่เรามีอยู่แล้วได้ด้วย เช่น เราอาจจะทำ Environment สำหรับการทำงานพวก Machine Learning ที่ลง Package ที่จำเป็นไว้แล้ว พอเราจะใช้งาน เราก็ Duplicate มันออกมาเพื่อใช้งานก็ได้เหมือนกัน แต่เราไม่ค่อยแนะนำ เพราะอย่างที่เราเล่าไปแล้วว่า Environment มันมีทั้ง Python Execuable และ Package ที่ลงไว้หมดเลย ซึ่งมันก็กินพื้นที่อยู่ ถ้าเราใช้วิธีนี้ มันจะเสียพื้นที่ SSD หรือ HDD ของเราไปเลย มันมีวิธีที่ดีกว่านี้ในการจัดการ ซึ่งเราจะมาคุยกันในบทความนี้กัน

pip freeze

วิธีแรกที่เราจะจัดการคือ เราจะใช้อะไรที่มันมากับ Python เลยอย่าง pip เอง ซึ่งเป็น Package Manager ของ Python เอง ทำให้ข้อดีอย่างแรกเลยคือ เราไม่ต้องลงอะไรเพิ่มเลย เพื่อให้เราใช้งานได้ วิธีการก็คือ ใน pip มันจะมีคำสั่ง pip freeze อยู่ มันจะพ่นพวก Package ที่เราลงไว้ใน Environment ของเราออกมา พร้อมกับ Version ที่เราใช้งานเลย ทำให้เรามั่นใจได้เลยว่า ถ้าเราไปลงในเครื่องอื่น ๆ เราก็จะได้ Version เดียวกันแน่นอน

แต่ pip freeze เฉย ๆ มันก็จะพ่นออกมาทาง stdout เลย ทำให้เราจะต้อง Pipe มันไปลง File เลย ทำให้คำสั่งมันก็จะเป็น pip freeze > requirements.txt นั่นเอง

เวลาเราไปเครื่องอื่น เราก็แค่ Clone ลงมา แล้วก็รัน pip install -r requirements.txt ได้เลย มันก็จะเข้าไปอ่านสิ่งที่ pip freeze เขียนไว้ว่ามันต้องลงอะไรบ้าง แล้วก็ลงให้เราเสร็จเลยพร้อมใช้งาน

ข้อดีของวิธีนี้อย่างที่เราบอกคือ เราไม่จำเป็นต้องลงอะไรเพิ่มเลย และมันก็ง่ายมาก ๆ ทั้งหมดถูกทำมาเป็น File Text พร้อมใช้งานได้เลย ถือว่าทำให้ง่ายสุด ๆ แต่ ๆๆ มันก็มาพร้อมกับข้อเสียเช่นกัน เพราะบางครั้ง Package บางตัวอย่าง Tensorflow มันก็จะมี Requirement เช่น มันใช้ได้กับแค่ Python ถึง Version 3.9 เท่านั้น หรือถ้าเราใช้ Python ที่เก่ากว่านี้ เราก็จะต้องใช้ Tensorflow Version ที่เก่าลงไป ทำให้ถ้า Python Version อีกเครื่องนึง มันไม่ตรงกับตอนที่เรา Freeze ไว้ บางครั้ง บาง Package มันอาจจะเกิดปัญหาลงไม่ได้เลยก็มีเหมือนกัน ดังนั้น วิธีนี้ เราจะต้องค่อนข้างมั่นใจว่า Python Version มันตรงกัน เพราะมันไม่มีอะไรมาคุม

กับอีกเรื่องคือ ถ้าเราทำงานกับ Environment ด้วย เราก็ต้องสร้าง Environment เองแล้วค่อยเรียก pip install ขึ้นมาถึงจะได้ ก็อาจจะมองเป็นข้อเสียก็ได้แหละ แต่อีกมุม ก็ทำให้เรามีอิสระในการเลือก Environment Manager ได้นั่นเอง

Conda env export

Conda นอกจากจะจัดการพวก Environment ให้เราได้แล้ว มันยังสามารถที่จะ Export และ Import Environment เข้ามาได้เหมือนกัน เราสามารถทำผ่านคำสั่ง conda env export มันจะพ่นผ่าน stdout เหมือนเดิม แต่ถ้าเราลองทำแล้วลองสังเกตดูคือ มันจะไม่ได้พ่นออกมาเป็นแค่ Package พร้อม Version เฉย ๆ ละ มันจะมีข้อมูลของชื่อ Environment มาให้เราเลย ทำให้เวลาเราไปใส่ในอีกเครื่องมันก็จะการันตีว่า จะเป็นชื่อเดียวกันแน่ ๆ เวลาเราทำ Command อะไรมาเรียก เราก็จะได้มั่นใจว่า มันเป็นคำสั่งเดียวกันในการ Activate แน่นอน

อีกส่วนที่มันใส่มาด้วย คือ Channel เพราะใน Conda เขาเป็น Package Manager ที่จะแบ่ง Package ออกเป็นหลาย ๆ Channel อย่างของเรา จะมี Apple ที่มันจะรวมพวก Tensorflow Dependencies ของ Apple ที่ทำให้เราสามารถเรียก GPU ผ่าน Metal อะไรพวกนั้นด้วย กับ conda-forge ที่เราใช้ เพราะเราทำงานผ่าน Apple Silicon ซึ่งใน Channel นั้นมันจะมี Package ที่เป็น ARM Version ด้วย

ถัดลงมา มันจะเป็น Package ละ มันก็จะ List ออกมาเลยว่ามันมีอะไรบ้าง Version อะไรบ้าง แล้วก็ Build Number ออกมาเพื่อให้ชัวร์ว่ามันคือตัวเดียวกันเป๊ะ ๆ อะไรแบบนั้น แต่ถ้าเราทำแบบนั้น ปัญหาคือ บางทีเราข้าม OS เช่น เราทำงานระหว่าง macOS และ Windows ถึงจะเป็น Version เดียวกัน แต่มันมักจะมี Build คนละตัวกันได้ด้วย ทำให้เราจะต้องไม่เอา Build Number มา เราสามารถเติม Argument --no-builds ลงไปได้

และสุดท้ายมันก็จะแบ่งให้เราอีกนะว่า Package ไหนเราติดตั้งผ่าน conda กับอันไหน เราติดตั้งผ่าน pip เพราะเอาจริง ๆ มันจะมีบาง Package ที่เราติดตั้งผ่าน conda ไม่ได้ แล้วต้องใช้ pip แทน มันก็แยกออกมาให้เราเลย

หรือ ๆ ถ้าเราบอกว่า เครื่องที่เราจะเอาไปรัน มันมี Configuration เดียวกันเลย เราอยากจะ Duplicate Environment แบบ 100% เลย เรายังสามารถกำหนดให้มัน Export แบบ Explicit ได้ด้วย ผ่านคำสั่ง conda list --explicit ที่แน่นอนว่ามันจะพ่นออกมาทาง stdout เราก็ต้อง pipe ใส่ไฟล์ด้วยเด้อ

ในการกำหนดแบบนี้ มันกำหนดยัน URL ที่ต้องไปดึง Package มาเลย ทำให้เรามั่นใจได้เลยว่า เราได้ Package ตัวเดียวกันแน่นอนเลยละ จากตัวอย่าง เราจะเห็นเลยว่า มันเป็น osx-arm64 หรือก็คือเป็น macOS ที่รันอยู่บน ARM 64-Bits หรือก็คือพวกตระกูล Apple Silcon นั่นเอง

สุดท้าย เมื่อเราต้องการจะ Import Environment เข้ามา เราก็สามารถเรียก conda env create -f  <ที่อยู่ไฟล์ที่ Export> เท่านี้เราก็จะได้ Environment ที่มีทุกอย่างเหมือนเดิมหมดเลยนั่นเองทำให้มันง่ายกว่าเดิมเยอะมาก

แต่ถ้าเรากำหนดมาเป็นแบบ Explicit เลย มันจะไม่มีพวกข้อมูลของชื่อ Environment มาให้ เวลาเราเรียกสร้าง เราจะต้องกำหนดชื่อให้มันด้วยผ่าน Argument --name ที่เราใช้ตอนสร้าง Environment นั่นเอง

จากทั้งหมดที่เราเล่ามา ทำให้ Conda จริง ๆ แล้วมันเป็นเหมือนกับ Jack of all trades เลยก็ว่าได้ เพราะมันทำได้ทุกอย่างตั้งแต่การ Isolate Environment ไปถึง Dependencies Management และ การ Import Export Environment ให้เราเสร็จเลย พร้อมกับตัวเลือกในการเลือกยันว่า ถ้าเราทำงานต่าง OS และสถาปัตยกรรม เราก็สามารถเลือกให้มันไม่เลือก Build และ Version ได้หมด แต่ ๆ แน่นอนว่า มีข้อดีต้องมีข้อเสียแน่นอน เพราะ Conda เวลาเราเจอ Project ที่ Dependencies เยอะ ๆ ก็มีช้ามาก ๆ เหมือนกัน โดยเฉพาะกับเครื่องช้า ๆ หน่อย เราเคยเจอลง Dependencies เกือบครึ่งชั่วโมงได้ คือ มันไม่ใช่เรื่องเท่าไหร่ที่เราจะลงกันครึ่งชั่วโมง

สรุป

การจัดการ Package และ Dependencies ต่าง ๆ เป็นเรื่องสำคัญมาก ๆ เพราะมันนำไปสู่เรื่ององ Reproducibility และ Upgradeability ของ Project เราโดยตรงเลย โดยเฉพาะ ถ้าเราจะแชร์ Project ของเราให้คนอื่นทำงานด้วยกัน หรือเรา Publish งานของเราออกไป การจัดการเรื่องพวกนี้เป็นเรื่องสำคัญมาก ๆ ซึ่งปัจจุบันมันก็มี Tools บน Python ให้เราใช้งานกันเยอะแล้วละ เช่น conda ที่เราค่อนข้างแนะนำเลย เพราะมันเป็นตัวที่ลงแค่ตัวเดียวแล้วจบเลย กับมันถูกใช้ค่อนข้างแพร่หลายมาก ๆ ทำให้เวลาเรามีปัญหา เราสามารถหาคนช่วยได้ง่าย หรือหาใน Stackoverflow ได้ง่าย นอกจากนั้น มันยังมีอีกตัวนึงที่น่าลองเหมือนกันคือ Poetry ลองไปเล่นกันดูได้ นอกจากนั้นมันยังมีพวก Dependencies Resolver อีกด้วย เช่น pip-tools ที่เข้ามาช่วยได้เยอะเวลาเราทำงานกับ Dependencies เยอะ ๆ