PEP 810 – การอิมพอร์ตแบบหน่วงเวลาอย่างชัดเจน
(pep-previews--4622.org.readthedocs.build)- โดยทั่วไปใน Python มีธรรมเนียมให้ประกาศ import ทั้งหมดไว้ที่ระดับโมดูล
- แต่เมื่อรันโปรแกรม มักมีการโหลดแม้แต่โมดูลdependency ที่ไม่จำเป็นทันที ทำให้เกิดปัญหาเรื่องความเร็วตอนเริ่มต้นและการใช้หน่วยความจำ
- ที่ผ่านมาแม้จะนิยมใช้การเลื่อน import ด้วยตนเอง เช่น ย้ายไปไว้ในฟังก์ชัน แต่ก็มีข้อเสียคือดูแลรักษายากและจัดการ dependency ได้ลำบาก
- PEP 810 ฉบับนี้เสนอไวยากรณ์ explicit lazy import ด้วยคีย์เวิร์ดใหม่
lazyที่มีลักษณะ local, explicit, controlled, granular - ฟีเจอร์นี้ช่วยให้โหลดโมดูลเฉพาะเมื่อจำเป็นต้องใช้จริง พร้อมทั้งลดเวลาเริ่มต้นและการสิ้นเปลืองหน่วยความจำ และยังคงความโปร่งใสของโครงสร้างโค้ดไว้ได้พร้อมกัน
สถานะปัจจุบันและปัญหาของการอิมพอร์ตใน Python
- ใน Python มีแนวปฏิบัติที่ใช้กันอย่างกว้างขวางให้เขียนคำสั่ง import ไว้ด้านบนสุดของโมดูล
- วิธีนี้ช่วยลดความซ้ำซ้อน มองเห็นโครงสร้าง dependency ของ import ได้ในภาพรวม และ import เพียงครั้งเดียวเพื่อลด runtime overhead
- แต่เมื่อโปรแกรมเริ่มทำงานและโมดูลแรก (main) ถูกโหลด ก็มักเกิดการอิมพอร์ตต่อเนื่องเป็นลูกโซ่จนไปอ่านแม้แต่โมดูล dependency จำนวนมากที่ไม่ได้ใช้งานจริงทันที
- โดยเฉพาะในเครื่องมือ CLI แค่เรียกดู help ทั้งหมดก็อาจทำให้มีการ preload โมดูลหลายสิบตัวล่วงหน้า ก่อให้เกิด overhead ที่ไม่จำเป็นในทุก subcommand
ทางเลือกเดิมและปัญหา
- มักมีการเลื่อนเวลาการ import ด้วยตนเอง เช่น ย้าย import ไปไว้ภายในฟังก์ชัน
- แต่แนวทางนี้มีข้อเสียมาก เช่น ความสม่ำเสมอลดลง ดูแลรักษายากขึ้น และทำให้มองภาพ dependency ทั้งหมดได้ยาก
- จากการวิเคราะห์ standard library พบว่า ในโค้ดที่ไวต่อประสิทธิภาพ มี import ราว 17% ของทั้งหมดที่ถูกวางไว้ภายในฟังก์ชันหรือเมธอดเพื่อจุดประสงค์ของการหน่วง import
- แม้จะมีเครื่องมือที่เกี่ยวข้อง เช่น
importlib.util.LazyLoaderและแพ็กเกจภายนอกlazy_loaderแต่ก็ยังไม่ครอบคลุมทุกกรณี หรือไม่มีมาตรฐานกลางเดียว
PEP 810: เพิ่ม explicit lazy import
-
เพิ่ม soft keyword ใหม่ชื่อ
lazy(มีความหมายเฉพาะบางบริบท และยังใช้เป็นชื่อตัวแปรได้) -
lazyใช้ได้เฉพาะหน้าคำสั่ง import เท่านั้น ใช้กับขอบเขตอย่างฟังก์ชัน/คลาส/with/try หรือ star import ไม่ได้ -
ทำให้สามารถกำหนดเป็นรายคำสั่ง import อย่างชัดเจน เพื่อเลื่อนการโหลดโมดูลออกไปจนกว่าจะถึงเวลาที่ใช้งาน
lazy import โมดูลชื่อ lazy from โมดูลชื่อ import ชื่อ
วิธีการทำงานของ explicit lazy import และกฎทางไวยากรณ์
-
กรณีที่เป็น syntax error:
- ใช้ภายในฟังก์ชัน, ภายในคลาส, ใน try/with, และ star import (
*) ไม่ได้ทั้งหมด
- ใช้ภายในฟังก์ชัน, ภายในคลาส, ใน try/with, และ star import (
-
ตัวอย่างการใช้งาน:
import sys lazy import json print('json' in sys.modules) # False (ยังไม่ถูกโหลด) result = json.dumps({"hello": "world"}) # โหลดเมื่อมีการใช้งานครั้งแรก print('json' in sys.modules) # True (โหลดโมดูลแบบหน่วงเวลาเสร็จแล้ว) -
สามารถระบุเป้าหมาย lazy เป็นรายโมดูลผ่านแอตทริบิวต์
__lazy_modules__ในรูปแบบลิสต์ของสตริงได้__lazy_modules__ = ["json"] import json # ถูกจัดการเป็น lazy
ควบคุมพฤติกรรมผ่าน global flag และ filter
-
สามารถใช้ global flag หรือฟังก์ชัน filter เพื่อควบคุมได้ว่าจะใช้ lazy ในระดับโมดูลหรือทั้งระบบหรือไม่
-
และใช้ฟังก์ชัน filter เพื่อกำหนดข้อยกเว้นให้บางโมดูลยังคงเป็น eager import ได้
def my_filter(importer, name, fromlist): if name in {'problematic_module'}: return False # eager import return True # lazy import sys.set_lazy_imports_filter(my_filter)
พฤติกรรมขณะรันไทม์และการจัดการข้อผิดพลาด
-
เมื่อใช้ lazy import จะยังไม่เกิดการ import จริงตอนเจอคำสั่ง import แต่จะเกิดขึ้นตอนเข้าถึงชื่อนั้นเป็นครั้งแรก
-
หาก import ล้มเหลว จะมีการใช้exception chain (traceback chaining) เพื่อแสดงทั้งตำแหน่งที่ประกาศและตำแหน่งที่เกิดข้อผิดพลาดอย่างชัดเจน
lazy from json import dumsp # พิมพ์ชื่อผิด result = dumsp({"key": "value"}) # เกิด ImportError ตอนมีการเข้าถึงจริง
ประโยชน์ด้านหน่วยความจำและประสิทธิภาพ
- โมดูลที่ถูกหน่วงจะปรากฏอยู่แค่ในเซ็ต sys.lazy_modules และยังไม่ถูกลงทะเบียนใน sys.modules จนกว่าจะมีการใช้งานจริง
- หลังจากใช้งานแล้วจะถูกแทนที่ด้วยออบเจ็กต์โมดูลปกติ และใช้งานต่อได้โดยไม่มี performance penalty เพิ่มเติม
- ในสภาพแวดล้อม workload จริง พบว่าเวลาเริ่มต้นลดลง 50~70% และหน่วยความจำลดลง 30~40%
สรุปกลไกการทำงาน
- เมื่อมีการเข้าถึง lazy object ครั้งแรก จะเกิด reification (import จริงและแทนที่ออบเจ็กต์)
- หากโค้ดภายนอกเข้าถึง
__dict__ของโมดูล จะมีการบังคับโหลด lazy object ทั้งหมด (reification) - เมื่อตรวจดึงดิกชันนารีด้วย
globals()จะยังคงได้ lazy proxy อยู่ และต้องเข้าถึงโดยตรง
type annotation และการเพิ่มประสิทธิภาพของ TYPE_CHECKING
- การใช้
lazy from โมดูล import ชื่อกับ import ที่ใช้เพื่อ type เท่านั้น ช่วยรับประกันว่าไม่มี runtime cost เลย - ทำให้สามารถแทนที่เงื่อนไข
from typing import TYPE_CHECKINGแบบเดิม และทำให้โค้ดกระชับและชัดเจนขึ้น
ความแตกต่างจาก PEP 690 และลักษณะเด่นของการออกแบบ
- PEP 810 เป็นโครงสร้างแบบ opt-in ที่ชัดเจน เป็นราย import และอิง proxy object แบบเรียบง่าย
- ขณะที่ PEP 690 ใช้โครงสร้าง lazy import แบบ global และ implicit
ข้อควรระวังและปฏิสัมพันธ์ระหว่างโมดูล
- star import (
*) ยังไม่รองรับแบบ lazy (เป็น eager เสมอ) - custom import hook และ loader จะยังทำงานตามปกติในจังหวะของ reification
- แม้อยู่ในสภาพแวดล้อม multithread ก็ยังรับประกันได้ว่าจะ import เพียงครั้งเดียวอย่างthread-safe และมีการ bind ที่ปลอดภัย
- หากมีการใช้ทั้ง lazy และ eager กับโมดูลเดียวกันพร้อมกัน ฝั่ง eager จะมีลำดับความสำคัญเสมอ
แนวทางการนำไปใช้และการย้ายระบบ
- เมื่อนำไปใช้กับโค้ดเดิม แนะนำให้ทำ profiling ก่อน แล้วค่อยแปลงเฉพาะ import ที่จำเป็นเป็น lazy แบบค่อยเป็นค่อยไป
- หากใช้
__lazy_modules__จะยังรองรับการทำงานร่วมกับ Python เวอร์ชันต่ำกว่า 3.15 ได้
ประเด็นคำถาม-คำตอบสำคัญอื่น ๆ
- side effect ตอน import (เช่น รูปแบบการ register) จะถูกเลื่อนออกไปจนกว่าจะมีการเข้าถึงครั้งแรก หาก side effect จำเป็นต่อการทำงาน แนะนำให้ใช้แพตเทิร์นฟังก์ชัน initialize แบบ explicit
- ปัญหา circular import ไม่ได้ถูกแก้ได้สมบูรณ์ด้วย lazy import (จะบรรเทาได้ก็ต่อเมื่อการเข้าถึงถูกเลื่อนออกไป)
- ประสิทธิภาพใน hot path หลังใช้งานครั้งแรกจะไม่มีการตรวจ lazy หลงเหลืออยู่เลย และมีการ optimize อัตโนมัติ (bytecode adaptive specialization)
- โมดูลจริงจะถูกลงทะเบียนใน
sys.modulesหลัง reification (การใช้งานครั้งแรก) เท่านั้น - แตกต่างจาก
importlib.util.LazyLoaderตรงที่ไม่ต้องมีการตั้งค่าเพิ่มเติม รักษาประสิทธิภาพไว้ได้ และมีความชัดเจนของ standard syntax
บทสรุป
- PEP 810 เพิ่มคีย์เวิร์ด
lazyให้กับคำสั่ง import ของ Python เพื่อช่วยเพิ่มประสิทธิภาพอย่างกระชับและคาดการณ์ได้ สำหรับปัญหาจากการโหลดโมดูลที่ไม่จำเป็นในงานอย่าง CLI แบบ subcommand, แอปพลิเคชันขนาดใหญ่ และ type annotation - คีย์เวิร์ดใหม่นี้สามารถกำหนดจุดเริ่มใช้และเป้าหมายได้อย่างละเอียด จึงเหมาะกับการทยอยนำไปใช้จริงและปรับจูนประสิทธิภาพในระบบโปรดักชัน
- นี่คือวิวัฒนาการที่จับต้องได้ของระบบ import ใน Python ซึ่งตอบโจทย์ทั้งความชัดเจน การดูแลรักษา และประสิทธิภาพไปพร้อมกัน
1 ความคิดเห็น
ความคิดเห็นบน Hacker News
เครื่องมือ CLI llm.datasette.io ของฉันรองรับปลั๊กอิน แต่มีคนบ่นกันมากว่าเวลาเริ่มต้นช้าเกินไปแม้แต่กับคำสั่งอย่าง
llm --helpพอตรวจดูก็พบว่าปลั๊กอินยอดนิยมหลายตัวimportแพ็กเกจหนัก ๆ อย่าง pytorch โดยปริยาย ทำให้การเริ่มต้นทั้งหมดติดค้างไปด้วย เลยแนะนำไว้ในเอกสารสำหรับผู้เขียนปลั๊กอินว่าให้importdependency ภายในฟังก์ชันเฉพาะตอนที่จำเป็นเท่านั้น (ลิงก์เอกสารที่เกี่ยวข้อง) แต่คิดว่าถ้ารองรับเรื่องนี้ในระดับภาษา Python เลยก็น่าจะดีกว่ามากวันนี้จะลองทำฟีเจอร์นี้ในเครื่องมือเลยก็ยังได้ (ลิงก์คำอธิบาย) เพียงแต่วิธีนี้จะถูกใช้แบบ global ทั้งโปรเซส ดังนั้นถ้าทำให้การ
importnumpy เป็นแบบช้า การimportsubmodule ทั้งหมดก็จะช้าตามไปด้วย สุดท้ายถ้าไม่ต้องใช้ numpy ทั้งก้อนก็อาจไม่ถูกimportเลย แต่ปรากฏการณ์ที่โมดูลถูกimportแบบหน่วงบางส่วนเมื่อถึงเวลาจำเป็นอาจกระจายอยู่ทั่ว runtime แบบคาดเดาไม่ได้ จากการทดลองเพิ่มพบว่า ถ้าimport foo.bar.bazนั้น foo กับ foo.bar จะยังถูกโหลดทันที และมีแค่ foo.bar.baz ที่หน่วงไว้ นี่น่าจะเป็นส่วนหนึ่งของเหตุผลที่ PEP ใช้คำว่า "mostly" ถ้าปรับปรุง implementation ของฉันต่อก็น่าจะแก้จุดนี้ได้แนะนำให้ parse command line ก่อน เพื่อให้ตัวเลือกอย่าง
--helpจัดการได้โดยไม่ต้องimportอะไรเลย แล้วค่อยimportเมื่อจำเป็นจริง ๆ หรือพูดง่าย ๆ คือออกแบบให้importเฉพาะเมื่อจัดการตัวเลือกคำสั่งง่าย ๆ เสร็จแล้วและยังมีงานต้องทำต่อข้อเสนอเรื่อง lazy import เคยมีมาแล้วในอดีต และล่าสุดก็ถูกปฏิเสธในปี 2022 (ลิงก์อภิปรายที่เกี่ยวข้อง) เท่าที่จำได้ lazy import มีอยู่แล้วใน Cinder ซึ่งเป็น CPython สายดัดแปลงของ Meta และ PEP ครั้งนี้ก็มีคนที่ทำงานกับ Cinder เป็นแกนนำ การถกเถียงโฟกัสไปที่ "จะเป็น opt-in หรือ opt-out?" "ขอบเขตการใช้ครอบคลุมแค่ไหน?" "ควรใส่เป็น build flag ของ CPython ไหม?" เป็นต้น สุดท้าย Steering Council ปฏิเสธเพราะความซับซ้อนที่เกิดจากพฤติกรรมของ
importที่แยกเป็นสองแบบ หวังว่าข้อเสนอรอบนี้จะผ่านจริง ๆ อยากใช้ฟีเจอร์นี้มากชอบมากตรงที่เป็นแบบ opt-in และยังมีการกำหนดขอบเขตการใช้ได้ละเอียดตามระดับต่าง ๆ พร้อม global kill switch ด้วย เป็นสเปกที่ประกอบขึ้นมาได้ดีมากภายใต้ข้อจำกัดหลายอย่าง
ฉันก็หวังว่าข้อเสนอนี้จะผ่าน แต่ไม่ได้มองโลกสวยนัก มันจะทำให้โค้ดจำนวนมหาศาลพัง และจะปล่อยปัญหาที่คาดไม่ถึงออกมาอีกมาก คำสั่ง
importมี side effect โดยพื้นฐานอยู่แล้ว และถ้าจังหวะเวลาที่มันเกิดเปลี่ยนไป ก็จะต้องทรมานกับบั๊กหาสาเหตุไม่เจอไปอีกนาน นี่ไม่ใช่การสร้างความกลัวเกินเหตุ แต่เป็นความกังวลที่มีเหตุผลจริง ๆ ที่ lazy import อยู่แค่ใน Meta ก็มีเหตุผลอยู่—มันเป็นเรื่องที่ดูจะรับมือได้เฉพาะองค์กรที่มีทรัพยากรมากระดับ Meta เท่านั้น หลายคนมองแค่ว่า "pandas, numpy หรือ weird module พันกันยุ่งของฉันมันช้า อยากให้เร็วขึ้น" แต่ฉันคิดว่ามีคนจำนวนน้อยมากที่เข้าใจด้วยซ้ำว่าระบบimportของ Python ทำงานอย่างไร แม้แต่คนที่สนับสนุนจำนวนมากก็ยังไม่รู้วิธี implement lazy import พอดู PEP 690 จะเห็นข้อเสียหลายอย่าง—ตัวอย่างเช่น โค้ดที่ใช้ decorator เพื่อเพิ่มฟังก์ชันเข้า central registry จะพัง โดยเฉพาะไลบรารี Dash ที่ผูก interface ฝั่ง JavaScript กับ Python callback ด้วย decorator ตอนimportถ้าimportกลายเป็นแบบ lazy frontend แบบนี้ก็อาจพังไปเลย บริการที่มีผู้ใช้จำนวนมากก็อาจเสียหายทันที คนชอบพูดว่า “ก็เป็น opt-in ถ้าไม่เหมาะก็ปิด lazy import ไปสิ” แล้วถ้าimportมันเป็นแบบ transitive ล่ะ? แล้วถ้าต้องเริ่ม process สำคัญหลังจาก frontend ถูก initialize สมบูรณ์แล้วล่ะ? ใน ecosystem ที่มีทั้งโค้ดและไลบรารีของคนหลายฝ่ายพันกัน ใครจะรู้ได้ว่าผลกระทบจะไปถึงไหน? มันต่างจาก type hint เพราะนี่คือการเปลี่ยนแปลงที่มีผลต่อพฤติกรรม runtime จริง ๆ และคำสั่งimportก็อยู่ในโค้ด Python แทบทุกชิ้น เพราะงั้นถ้ามี lazy import เข้ามา วิธีการรันก็จะเปลี่ยนไปในระดับพื้นฐาน นอกจากนี้ยังมีเคสแปลก ๆ อีกหลายอย่างที่ PEP พูดถึง เป็นปัญหาที่ยากกว่าที่คิดมากถ้ามี
import torch==2.6.0+cu124,import numpy>=1.2.6แบบระบุเวอร์ชันได้ และสามารถติดตั้ง/importแพ็กเกจหลายเวอร์ชันพร้อมกันใน Python environment เดียวได้ก็คงดีมาก อยากเลิกนรก conda/virtualenv/docker/bazel ได้แล้วสักทีไม่ได้เกลียดเท่าไร แต่ก็ไม่ได้ต้อนรับสุด ๆ ถ้าเป็นแบบนี้คงต้องใส่
lazyหน้าimportแทบทุกอัน ยกเว้นบางกรณีที่ต้อง eager import จริง ๆ สุดท้ายก็ต้องแปะ lazy เต็มไปหมดจนโค้ดรก และเพราะไม่ได้มีแผนจะให้มันกลายเป็นพฤติกรรมปริยาย ความรกรุงรังนี้ก็จะอยู่ไปตลอด ฉันกลับคิดว่าน่าจะดีกว่าถ้าให้ฝั่งโมดูลประกาศ opt-in สำหรับ lazy loading เอง โดยไม่ต้องเปลี่ยน syntax ของimportแบบนั้นจะมีแค่ไลบรารีขนาดใหญ่ที่ต้องสนใจเรื่อง laziness แน่นอนว่าถ้าทำแบบนั้น interpreter ก็ต้องไปไล่ดู file system ตอนimportและยังมีข้อเสียอื่นอีก แต่ก็ว่าไปอย่างถ้าทุกคนใช้ lazy import กันเยอะได้โดยแทบไม่มีปัญหา แปลว่า lazy ควรเป็นค่าเริ่มต้น และ <i>eager</i> ต่างหากที่ควรเป็นคีย์เวิร์ดแบบเลือกใช้ การเปลี่ยนพาราไดม์แบบนี้ก็ไม่ใช่ครั้งแรกของ Python ใน v2 syntax หลายอย่างสร้าง list แบบ eager แต่ใน v3 เปลี่ยนเป็น generator แล้วก็ไม่ได้มีปัญหาใหญ่อะไร
ถ้ามี command-line flag ที่ทำให้การ
importโมดูลทั้งระบบของ Python กลายเป็น lazy ได้ ฉันจะใช้แน่นอน เพราะนอกจากสคริปต์หรือโค้ดง่ายมาก ๆ แล้ว การมี side effect ตอน module load เป็นแพตเทิร์นที่ควรหลีกเลี่ยงจริง ๆไม่คิดว่าควรให้ฝั่งโมดูลเป็นคนตัดสินว่าจะ lazy load หรือไม่ มีแค่ฝั่งผู้เรียกเท่านั้นที่รู้ว่าต้องการ lazy load หรือเปล่า ดังนั้นให้ฝั่งโค้ดที่
importเป็นคนกำหนดตัวเลือกจึงสมเหตุสมผลกว่า โมดูลไหน ๆ ก็โหลดแบบ lazy ได้ และแม้มี side effect ผู้เรียกก็อาจอยากหน่วงมันออกไปอยู่ดีอยากให้ระบุตัวเลือก lazy loading ด้วย regex ใน
pyproject.tomlได้ในอดีตตอนมีฟีเจอร์ใหม่อย่าง type hint, walrus, asyncio, dataclasses ฯลฯ คนก็เคยกังวลคล้าย ๆ กัน แต่ในความจริงก็ไม่ได้มีคนจำนวนมากขนาดนั้นที่ใช้ทุกอย่างพร้อมกันหรือเปลี่ยนแพตเทิร์นเดิมทั้งหมด ผู้ใช้จำนวนมากทุกวันนี้ก็ยังใช้แค่ความสามารถระดับ Python 2.4 ที่ถูกทำให้ทันสมัยขึ้นนิดหน่อยเท่านั้น และก็ยัง productive ได้ดี ผ่านมา 20 ปีระบบก็ยังไปได้ดี เลยมองว่าไม่น่ามีปัญหาใหญ่อะไร
ถ้าสนใจ ขอแนะนำ lazyimp ที่ทำ lazy import ในรูปแบบ context manager ได้สะดวกมาก โดยปกติก็แค่ครอบคำสั่ง
importด้วยบล็อก with จึงเข้ากับเครื่องมือเดิมได้ดี และถ้าต้อง debug ก็สลับกลับไป eager import ได้ง่าย มันใช้ cext ไปเปลี่ยน f_builtins ของ frame เลยทำได้ทรงพลังกว่า importlib hook แม้ไม่สมบูรณ์แบบแต่ก็มีทั้งเวอร์ชัน thread-safe และเวอร์ชัน global handler ตอนแรกฉันก็ระวังตัวอยู่ แต่ตอนนี้ย้ายโค้ดเกือบทั้ง codebase มาใช้มันแล้ว และยังไม่เจอปัญหาอะไรจริง ๆ เลย (นอกจากเคยลืมดูแลเรื่องการลงทะเบียนรายโมดูล) แถมความเร็วที่รู้สึกได้ก็มหาศาล เลยพอใจมากน่ารำคาญมากที่ Python linter มักบังคับให้วาง
importไว้บนสุดของไฟล์ ทุกครั้งที่ใช้วิธีทำ lazy import แบบตรงไปตรงมาก็จะโดน lint error เรื่องนี้เป็นมากกว่าปัญหาด้าน performance เสียอีก เช่น เวลาต้องใช้ไลบรารีเฉพาะแพลตฟอร์ม เราอาจอยากimportเฉพาะบนแพลตฟอร์มนั้น แต่ถ้าบังคับให้importด้านบน ไฟล์อาจimportไม่ผ่านตั้งแต่แรกได้เลยแบบนั้นก็คงต้องไปแก้ linter อย่างเดียว
linter ส่วนใหญ่สั่ง ignore ได้ด้วยคอมเมนต์อย่าง
#noqa E402วิธีนี้คือแทนที่ meta path finder ด้วยตัวห่อ ทำให้ loader ถูกแทนด้วย LazyLoader ตอน
importชื่อโมดูลจริง ๆ จะถูก bind เป็น<class 'importlib.util._LazyModule'>และพอเข้าถึง attribute จึงค่อยโหลดโมดูลจริง โค้ดทดลอง:แต่ก็ยังไม่แน่ใจว่าคำว่า "mostly" ใน PEP หมายถึงอะไรอย่างแม่นยำ
ดูเหมือนจะประเมินความเสี่ยงด้าน thread safety ของ lazy import ต่ำเกินไป ไม่สามารถคาดเดาได้เลยว่า
importจะไปรันเมื่อไร ในเธรดไหน และถือ lock อะไรอยู่บ้าง นอกจาก importer lock แล้วก็รับประกันอะไรไม่ได้ เมื่อก่อนถึงแม้ตอนimportโมดูลจะมีโค้ดเสี่ยง ๆ ทำงานอยู่ แต่ส่วนใหญ่ก็มักเกิดในขั้นตอน initialize แบบ single-threaded เลยยังไม่เป็นปัญหาใหญ่ พอเปลี่ยนเป็น lazy แล้ว error จะเด้งออกมาในรูปแบบคาดเดาไม่ได้จริง ๆ แบบ Heisenbug แม้importระดับฟังก์ชันก็มีความเสี่ยงแนวนี้อยู่เหมือนกัน แต่ยังพอคาดเดาได้อย่างน้อยว่ามันจะรันตอนต้นของโค้ดที่เขียนไว้อย่างชัดเจนรู้สึกว่าเป็นฟีเจอร์ที่ดี อธิบายก็ง่าย มี use case จริง และขอบเขต (ทั้งแบบ global และแบบคีย์เวิร์ดง่าย ๆ) ก็พอดี ชอบเลย
ในบรรดา PEP ช่วงหลัง ๆ อันนี้ให้ความรู้สึกว่าคลีนที่สุดในมุมผู้ใช้ รอดูผลลัพธ์จริงหลังผ่านกระบวนการ syntax bikeshedding แบบดั้งเดิมนี้
คิดว่าเป็น PEP ที่เตรียมมาอย่างรอบคอบ ทั้งการตรวจสอบกรณีใช้งานจริงกับ edge case การประนีประนอมที่เหมาะสม วิธีการที่ไม่มากเกินไป และการขัดเกลาหลายรอบ โดยเฉพาะเมื่อเป็นการไปแตะระบบแกนหลักของภาษาขนาดใหญ่ที่มีชุมชนหลากหลายทั่วโลก จึงอันตรายได้มาก ยิ่งพิจารณาความยากตรงนั้นก็ยิ่งน่าประทับใจ
หวังว่าพวกเขาจะได้เรียนรู้เพียงพอแล้วจากเหตุผลที่ PEP-690 ถูกปฏิเสธ ใน codebase ของเราก็เคยพยายามทำฟีเจอร์แบบนี้เอง แต่ไม่เคยทำให้มันทำงานดีพอใช้งานจริงได้เลย
อันตรายของ lazy import คือมันมีแนวโน้มสร้าง runtime error ที่คาดไม่ถึงในบริการที่รันยาวนาน แม้มันจะดูเหมือนเป็นข้อดีเรื่อง startup ที่เร็วขึ้น แต่ก็เป็น tradeoff ที่ต้องยอมรับความเสี่ยงว่าโค้ดอาจหยุดกลางคันเพราะ
importล้มเหลว นอกจากนี้ยังอาจมี edge case ที่ทำให้ไม่สามารถยืนยันได้แน่ชัดว่าตอนเริ่มโปรแกรมจะมีอะไรถูกimportบ้างถึงอย่างนั้นนี่ก็เป็นปัญหาจริงที่ต้องแก้ ไม่ใช่แค่เรื่อง startup speed เพราะถ้า Python startup ต้องลาก dependency ใหญ่ ๆ เข้ามา มันจะช้าจนเกินเหตุ โปรเจกต์ใหญ่ก็ไม่สามารถ bundle ไลบรารีหนัก ๆ ทั้งหมดที่ผู้ใช้ทุกคนไม่ได้ใช้ได้อยู่แล้ว นักพัฒนาจึงใช้วิธีอ้อมที่ประหลาดกว่านี้อยู่แล้ว และนั่นก็ยิ่งเพิ่มปัญหาไร้สาระเข้าไปอีก แค่ช่วยลดความลำบากจากการต้องซ่อน
importระดับฟังก์ชันไว้ซ้ำ ๆ ก็ถือว่าก้าวหน้าแล้ว และนี่ก็ถูกเสนอในฐานะฟีเจอร์ภาษาที่เป็นทางเลือกเท่านั้นใช้ automated test ก็บรรเทาความเสี่ยงได้มากพอ และคุ้มค่ากับ startup ที่เร็วขึ้น เวลา startup ไม่ได้เป็นแค่ปัญหา "ภาพลักษณ์" เลย ฉันเคยเจอปัญหานี้ใน Django monolith ที่ต้องรอ 10–15 วินาทีทุกครั้งกับ management command, test, และการ reload คอนเทนเนอร์ เพียงเพราะมีไลบรารีหนัก ๆ ไม่กี่ตัว พอใช้ lazy import เพื่อ defer ออกไป ความต่างมหาศาลมาก
พวกเราโดยทั่วไปชอบ
importแบบชัดเจนไว้บนสุด เพราะต้องการให้ปัญหา dependency โผล่มาตั้งแต่เริ่มโปรแกรม ถ้าใช้ lazy import ก็จะไปเจอปัญหาก็ต่อเมื่อเส้นทางโค้ดนั้นถูกรัน (อาจหลายชั่วโมงหรือหลายวันให้หลัง)importทุกอันถูก defer อัตโนมัติ งานสั้น ๆ ก็น่าจะได้ประโยชน์เรื่องความเร็วของ pip ทันทีเวลาส่วนใหญ่หมดไปกับการ
importและ unload โมดูล vendor ที่จริง ๆ ไม่ได้ใช้เลย (เช่น เฉพาะโมดูลที่เกี่ยวกับ Requests ก็เกือบ 100 ตัวแล้ว) จากการวิเคราะห์พบว่ามีการimportโมดูลเกินจำเป็นมากกว่า 500 ตัวฉันไม่เข้าใจเหมือนกันว่าทำไม code generator ถึงสร้างโค้ดที่ใส่ local import ไว้ในฟังก์ชันแทนที่จะวาง
importไว้ด้านบนมากขึ้นเรื่อย ๆ ไม่อยากสนับสนุนแพตเทิร์นนี้ เพราะทำให้ดู dependency ของโมดูลได้ยาก และเพิ่มความเสี่ยงที่จะเกิด circular dependency ภายหลังยังอ่าน PEP ไม่จบ แต่คิดว่าถ้ามี dependency validation ผ่าน command-line flag หรือเครื่องมือภายนอกได้ก็คงดี คล้ายกับเครื่องมือที่มีให้กับ type hint
คำว่า "พวกเรา" นี่หมายถึงใครบ้างกันแน่
เรื่องนี้ไม่ควรเป็นสิ่งที่ครอบคลุมได้ด้วยการทดสอบหรือ?