PEP 810 – Impor Tertunda Eksplisit
(pep-previews--4622.org.readthedocs.build)- Dalam Python, sudah menjadi konvensi umum untuk mendeklarasikan semua impor di level modul
- Namun, saat program dijalankan, bahkan modul dependensi yang tidak diperlukan ikut langsung dimuat sehingga menimbulkan masalah pada kecepatan startup dan penggunaan memori
- Sebelumnya, banyak orang memakai impor tertunda secara manual seperti impor di dalam fungsi, tetapi pendekatan ini punya kelemahan berupa sulitnya pemeliharaan dan pengelolaan dependensi
- PEP 810 kali ini memperkenalkan sintaks impor tertunda eksplisit dengan kata kunci baru
lazyyang bersifat local, explicit, controlled, granular - Dengan fitur ini, modul hanya dimuat saat benar-benar dibutuhkan, sekaligus mewujudkan perbaikan latensi startup, pengurangan pemborosan memori, dan transparansi struktur kode
Kondisi Python import saat ini dan masalahnya
- Di Python, praktik menulis pernyataan import di bagian paling atas modul secara umum sudah luas digunakan
- Pendekatan ini mengurangi duplikasi, memudahkan melihat struktur dependensi impor dalam sekali pandang, dan meminimalkan overhead runtime dengan melakukan impor hanya sekali
- Namun, ketika program dijalankan dan modul pertama (main) dimuat, sering terjadi impor berantai yang langsung membaca banyak modul dependensi yang sebenarnya tidak digunakan
- Khususnya pada alat CLI, bahkan saat hanya memanggil help penuh, puluhan modul bisa dimuat lebih dulu, sehingga setiap subcommand menanggung overhead yang tidak perlu
Alternatif yang ada dan masalahnya
- Cara menunda waktu impor secara manual, misalnya dengan memindahkan impor ke dalam fungsi, sering digunakan
- Namun, pendekatan ini memiliki kelemahan besar seperti turunnya konsistensi dan maintainability, serta makin sulitnya memahami keseluruhan dependensi
- Hasil analisis standard library menunjukkan bahwa pada kode yang sensitif terhadap performa, sekitar 17% dari seluruh impor sudah digunakan di dalam fungsi atau metode untuk tujuan menunda impor
- Alat terkait penundaan impor seperti
importlib.util.LazyLoaderdan paket pihak ketigalazy_loadermemang ada, tetapi belum memenuhi semua kasus atau belum ada satu standar tunggal
PEP 810: memperkenalkan impor tertunda eksplisit
-
Memperkenalkan soft keyword baru
lazy(hanya bermakna dalam konteks tertentu, dan tetap bisa dipakai sebagai nama variabel, dll.) -
lazyhanya digunakan di depan pernyataan import, dan tidak bisa dipakai pada blok fungsi/kelas/with/try maupun star import -
Penggunaannya dibedakan secara jelas per pernyataan import, sehingga pemuatan modul ditunda sampai saat modul itu dipakai
lazy import nama_modul lazy from nama_modul import nama
Cara implementasi impor tertunda eksplisit dan syntactic rule
-
Kasus yang menghasilkan kesalahan sintaks:
- Tidak boleh di dalam fungsi, di dalam kelas, pada try/with, maupun star import (
*)
- Tidak boleh di dalam fungsi, di dalam kelas, pada try/with, maupun star import (
-
Contoh penggunaan:
import sys lazy import json print('json' in sys.modules) # False (belum dimuat) result = json.dumps({"hello": "world"}) # dimuat saat pertama kali digunakan print('json' in sys.modules) # True (pemuatan modul tertunda selesai) -
Di level modul, target lazy juga bisa dinyatakan sebagai daftar string di atribut
__lazy_modules____lazy_modules__ = ["json"] import json # diproses sebagai lazy
Kontrol perilaku lewat flag global dan filter
-
Dengan flag global atau fungsi filter, penerapan lazy bisa dikendalikan pada level modul maupun secara keseluruhan
-
Fungsi filter juga bisa digunakan untuk memberi pengecualian eager import hanya pada modul tertentu
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)
Perilaku runtime dan penanganan error
-
Saat menggunakan lazy import, impor yang sebenarnya terjadi bukan pada pernyataan import, melainkan pada saat nama tersebut pertama kali diakses
-
Jika impor gagal, exception chain (traceback chaining) akan menampilkan dengan jelas baik lokasi definisi maupun lokasi terjadinya error
lazy from json import dumsp # salah ketik result = dumsp({"key": "value"}) # ImportError terjadi saat benar-benar diakses
Keuntungan memori dan performa
- Modul yang ditunda hanya muncul di himpunan sys.lazy_modules, dan tidak didaftarkan ke sys.modules sebelum benar-benar digunakan
- Setelah digunakan, modul tersebut diganti menjadi objek modul normal dan bisa digunakan tanpa penalti performa tambahan
- Pada beban kerja nyata, terlihat efek penurunan latensi startup 50~70% dan penghematan memori 30~40%
Ringkasan cara kerjanya
- Saat lazy object pertama kali diakses, terjadi reification (impor aktual dan penggantian)
- Jika kode eksternal mengakses
__dict__milik modul, semua lazy object akan dipaksa dimuat (reification) - Saat mengambil dictionary dengan
globals(), lazy proxy tetap dipertahankan sehingga perlu diakses secara langsung
Type annotation dan optimasi TYPE_CHECKING
- Dengan
lazy from modul import nama, impor yang hanya dipakai untuk tipe dijamin memiliki biaya runtime ZERO - Ini dapat menggantikan pola kondisi
from typing import TYPE_CHECKINGyang ada sekarang, sehingga kode menjadi lebih ringkas dan jelas
Perbedaan dengan PEP 690 sebelumnya dan karakteristik implementasinya
- PEP 810 memakai struktur opt-in yang eksplisit, per impor individual, dan berbasis objek proxy sederhana
- Sebaliknya, PEP 690 memakai struktur lazy import yang global dan implisit
Hal-hal yang perlu diperhatikan dan interaksi antar modul
- star import (
*) tidak didukung untuk lazy (selalu eager) - import hook dan loader kustom tetap berjalan apa adanya pada saat reification
- Bahkan di lingkungan multithread, impor dan binding yang aman tetap dijamin hanya terjadi sekali secara thread-safe
- Jika lazy dan eager digunakan bersamaan untuk modul yang sama, sisi eager selalu diprioritaskan
Panduan penerapan kode dan migrasi
- Saat menerapkannya pada kode yang ada, disarankan melakukan profiling lalu mengubah hanya impor yang diperlukan menjadi lazy, secara bertahap
- Jika memanfaatkan
__lazy_modules__, kompatibilitas tetap terjaga bahkan pada versi sebelum Python 3.15
Poin tanya-jawab penting lainnya
- Efek samping saat waktu impor (misalnya pola registrasi) akan tertunda sampai akses pertama. Jika side effect wajib terjadi, disarankan memakai pola fungsi inisialisasi eksplisit
- Masalah circular import (impor sirkular) tidak bisa diselesaikan sepenuhnya dengan lazy import (hanya bisa berkurang jika aksesnya memang tertunda)
- Performa hot path setelah first use akan otomatis dioptimalkan karena pengecekan lazy sepenuhnya hilang (bytecode adaptive specialization)
- Di
sys.modules, modul nyata baru didaftarkan setelah reification (pemakaian pertama) - Berbeda dari
importlib.util.LazyLoader, tidak perlu konfigurasi tambahan, performa tetap terjaga, dan sintaks standarnya lebih jelas
Kesimpulan
- PEP 810 menambahkan kata kunci
lazyke pernyataan import Python, sehingga masalah performa akibat pemuatan modul yang tidak perlu dapat dioptimalkan secara ringkas dan dapat diprediksi di berbagai area seperti CLI subcommand, aplikasi besar, dan type annotation - Kata kunci baru ini memungkinkan penentuan waktu penerapan dan target secara detail, sehingga cocok untuk adopsi bertahap dan tuning performa di layanan nyata
- Ini adalah evolusi nyata pada sistem import Python yang sekaligus memenuhi tiga kebutuhan: visibilitas, maintainability, dan performa
1 komentar
Komentar Hacker News
Alat CLI llm.datasette.io saya mendukung plugin, tetapi banyak keluhan tentang waktu startup yang terlalu lambat bahkan untuk perintah seperti "llm --help". Setelah dicek, ternyata plugin populer secara default mengimpor paket berat seperti pytorch sehingga seluruh startup tertahan. Karena itu saya menambahkan panduan di dokumentasi penulis plugin agar dependensi diimpor hanya saat benar-benar diperlukan di dalam fungsi (tautan dokumentasi terkait), tetapi akan jauh lebih baik kalau masalah seperti ini didukung di tingkat bahasa Python
Sebenarnya fitur ini bisa diimplementasikan di alat hari ini juga (tautan penjelasan), hanya saja pendekatan ini berlaku global untuk seluruh proses, jadi kalau impor numpy dibuat lambat maka impor submodulnya juga ikut lambat. Hasil akhirnya, kalau seluruh numpy tidak dibutuhkan maka bisa jadi tidak diimpor sama sekali, tetapi gejala impor modul secara parsial saat diperlukan bisa tersebar secara tak terduga sepanjang runtime. Hasil eksperimen tambahan menunjukkan bahwa jika mengimpor seperti
import foo.bar.baz, maka foo dan foo.bar tetap dimuat segera dan hanya foo.bar.baz yang ditunda, mungkin ini salah satu alasan PEP memakai kata "mostly". Kalau saya lanjut menyempurnakan implementasi saya, mungkin ini juga bisa diatasiSaya menyarankan parsing command line lebih dulu agar opsi seperti "--help" bisa ditangani tanpa impor apa pun. Impor baru dijalankan saat benar-benar perlu, atau sederhananya, rancang agar impor hanya terjadi setelah opsi command yang mudah sudah diproses dan masih ada pekerjaan yang tersisa
Usulan lazy import pernah ada sebelumnya, dan yang terbaru ditolak pada 2022 (tautan diskusi terkait). Seingat saya, lazy import sudah ada di Cinder, varian CPython milik Meta, dan PEP kali ini juga dipimpin orang-orang yang mengerjakan Cinder. Fokus diskusinya antara lain "opt-in atau opt-out?" "cakupannya sampai mana?" "perlukah dimasukkan sebagai build flag CPython?" dan sebagainya. Pada akhirnya Steering Council menolak karena kompleksitas akibat perilaku import yang terbelah dua. Saya benar-benar berharap usulan kali ini lolos, saya sangat ingin memakai fitur ini
Saya khususnya suka karena ini pendekatan opt-in, dengan penerapan yang rinci per level dan bahkan ada sakelar global untuk mematikannya. Spesifikasinya terasa sangat matang dalam berbagai batasan yang ada
Saya juga berharap usulan ini lolos, tetapi saya tidak optimistis. Ini akan merusak banyak kode dan memunculkan banyak masalah tak terduga. Pernyataan import pada dasarnya punya efek samping, dan kalau waktu penerapannya berubah maka kita bisa menderita bug misterius dalam waktu lama. Ini bukan menakut-nakuti, ada alasan nyata untuk khawatir. Ada alasan mengapa lazy import hanya masuk ke Meta—karena mungkin hanya organisasi dengan sumber daya sebesar Meta yang bisa menanganinya. Banyak orang hanya melihat "pandas, numpy, atau weird module saya yang kusut terlalu lambat, semoga bisa dipercepat", padahal saya rasa sedikit sekali yang benar-benar paham bagaimana sistem import Python bekerja. Bahkan banyak yang mendukung tanpa tahu cara lazy import diimplementasikan. Kalau melihat PEP 690, ada banyak kekurangan—misalnya kode yang memakai dekorator untuk menambahkan fungsi ke registry pusat akan rusak. Contoh nyatanya, library Dash menghubungkan antarmuka berbasis JavaScript dan callback Python lewat dekorator saat import time; kalau import jadi lazy, frontend seperti ini bisa langsung mati total. Layanan dengan banyak pengguna pun bisa langsung rusak. Orang bilang, “karena ini opt-in, kalau tidak cocok tinggal matikan lazy import”. Tapi bagaimana kalau import-nya transitif? Bagaimana kalau proses penting baru boleh dimulai setelah frontend terinisialisasi penuh? Siapa yang tahu dampaknya dalam ekosistem yang dipenuhi kode dan library dari banyak pihak? Ini berbeda dari type hint karena benar-benar mengubah perilaku runtime. Pernyataan import ada di hampir semua kode Python yang nyata, jadi kalau lazy diperkenalkan, cara eksekusinya berubah secara fundamental. Masih ada juga kasus-kasus aneh lain yang disebut di PEP. Ini masalah yang jauh lebih sulit daripada kelihatannya
Akan sangat bagus kalau ada import dengan spesifikasi versi seperti
import torch==2.6.0+cu124,import numpy>=1.2.6, dan bisa memasang/mengimpor beberapa versi paket sekaligus dalam environment Python yang sama. Saya sudah ingin lepas dari neraka conda/virtualenv/docker/bazelSaya tidak terlalu membencinya, tetapi juga tidak terlalu antusias. Kalau begini, rasanya hampir semua import akan diberi
lazy, kecuali beberapa kasus yang memang harus eager, jadi kodenya malah berantakan. Dan karena ini juga tidak direncanakan menjadi perilaku default, kerepotan ini akan tinggal selamanya. Saya pribadi lebih suka kalau justru sisi modul yang bisa menyatakan opt-in untuk lazy loading, tanpa perubahan pada sintaks import. Dengan begitu, hanya library besar yang perlu memikirkan laziness. Tentu pendekatan itu juga punya kekurangan, misalnya interpreter harus menjelajahi file system saat import time, dan sebagainyaKalau semua orang bisa memakai lazy import secara luas tanpa masalah berarti lazy seharusnya menjadi default, dan justru <i>eager</i> yang menjadi keyword opsional. Perubahan paradigma seperti ini bukan hal baru di Python; di v2 ada banyak konstruksi yang membuat list secara eager, lalu di v3 berubah menjadi generator, dan ternyata tidak menimbulkan masalah besar
Jika ada flag command line untuk menjadikan seluruh import modul Python bersifat lazy, saya pasti akan memakainya. Dalam praktiknya, kecuali untuk skrip atau kode yang benar-benar sederhana, munculnya side effect saat module load itu pola yang seharusnya sangat dihindari
Menurut saya tidak tepat kalau modul yang menentukan apakah lazy loading dipakai atau tidak. Hanya pemanggil yang tahu apakah lazy load dibutuhkan, jadi masuk akal kalau opsinya diberikan dari sisi kode yang mengimpor. Modul apa pun bisa di-lazy-load, dan meskipun ada side effect, pemanggil mungkin memang ingin menundanya juga
Saya berharap ada cara untuk menyatakan opsi lazy loading dengan regex di pyproject.toml
Dulu setiap kali fitur baru seperti type hint, walrus, asyncio, dataclasses, dan sebagainya muncul, orang juga mengkhawatirkan hal serupa. Namun pada praktiknya tidak banyak orang yang langsung memakai semuanya sekaligus atau mengubah seluruh pola lama. Banyak pengguna masih hanya memakai fitur setara Python 2.4 yang sudah dimodernisasi, dan tetap sangat produktif. Sudah berjalan baik selama 20 tahun, jadi saya rasa tidak akan jadi masalah besar
Kalau tertarik, saya ingin memperkenalkan lazyimp, yang mengimplementasikan lazy import dengan sangat nyaman dalam bentuk context manager. Biasanya cukup membungkus pernyataan import dengan blok with, jadi tetap cocok dengan alat yang sudah ada. Kalau perlu debugging, mudah juga beralih kembali ke eager import. Dengan cext, ia mengganti f_builtins pada frame sehingga lebih kuat daripada hook importlib. Memang tidak sempurna, tetapi ada juga versi thread-safe dan versi handler global. Awalnya saya hati-hati, tetapi sekarang hampir seluruh codebase saya sudah dipindahkan ke ini, dan sejauh ini tidak ada masalah nyata sama sekali (selain saya sempat lupa menangani proses registrasi per modul). Peningkatan performanya sangat terasa dan saya puas
Sangat menjengkelkan bahwa linter Python memaksa import berada di bagian atas file. Setiap kali memakai cara lazy import yang jelas, saya kena error lint. Ini lebih dari sekadar isu performa. Misalnya, saat butuh library khusus platform, saya ingin mengimpornya hanya di platform tersebut, tetapi kalau import di atas dipaksa, import bisa langsung gagal total
Dalam kasus seperti itu, menurut saya memang linter-nya saja yang harus diperbaiki
Kebanyakan linter bisa diabaikan dengan komentar seperti
#noqa E402Dengan cara ini, meta path finder diganti dengan wrapper yang membungkusnya, lalu loader diganti menjadi LazyLoader. Saat import dijalankan, nama modul sebenarnya akan di-bind sebagai
<class 'importlib.util._LazyModule'>, lalu modul asli baru dimuat saat atributnya diakses. Kode eksperimennya:Namun saya tidak tahu persis apa arti kata "mostly" dalam PEP itu
Saya rasa risiko thread safety pada lazy import diremehkan. Sama sekali tidak bisa diprediksi kapan import akan dijalankan, di thread mana, dan sambil memegang lock apa. Selain lock importer, tidak ada jaminan apa pun. Dulu, walaupun ada kode berbahaya yang berjalan saat import modul, biasanya itu terjadi hanya dalam proses inisialisasi single-threaded sehingga tidak terlalu bermasalah. Jika diubah jadi lazy, error bisa muncul dalam pola yang benar-benar tak terduga, seperti Heisenbug. Import pada level fungsi juga punya potensi masalah ini, tetapi setidaknya masih ada prediktabilitas karena dijalankan secara eksplisit di bagian awal kode
Ini terasa seperti fitur yang bagus, penjelasannya mudah dipahami, kasus pemakaian nyatanya jelas, dan cakupannya juga pas (untuk penggunaan global maupun keyword sederhana). Saya suka
Di antara PEP yang belakangan muncul, ini terasa paling rapi dari sudut pandang pengguna. Setelah melewati proses bikeshedding sintaks tradisional ini, saya penasaran dengan hasil akhirnya
Saya rasa ini PEP yang disiapkan dengan teliti: ada validasi terhadap kasus praktis dan edge case, komprominya tepat, pendekatannya tidak berlebihan, dan terlihat sudah disempurnakan berkali-kali. Terutama karena ini menyentuh sistem inti sebuah bahasa besar dengan komunitas yang sangat beragam di seluruh dunia, yang bisa sangat berisiko, maka usaha itu terasa makin mengesankan
Saya berharap mereka sudah benar-benar belajar dari alasan penolakan PEP-690. Di codebase kami pun kami pernah mencoba mengimplementasikan fitur seperti ini sendiri, tetapi tidak pernah berhasil membuatnya bekerja dengan cukup baik untuk benar-benar layak dipakai
Bahaya lazy import adalah ia mudah menciptakan runtime error tak terduga pada layanan yang berjalan lama. Memang tampak menguntungkan karena startup jadi cepat, tetapi tradeoff-nya adalah program bisa berhenti di tengah eksekusi akibat kegagalan import. Selain itu, bisa muncul edge case di mana kita tak lagi bisa yakin apa saja yang akan diimpor saat program mulai
Meski begitu, ini masalah nyata yang memang harus diselesaikan. Ini bukan cuma soal kecepatan startup; startup Python bisa menjadi sangat lambat secara tidak masuk akal ketika dependensi besar ikut masuk. Proyek besar juga tidak bisa begitu saja membundel semua library berat yang bahkan tidak dipakai semua pengguna, sehingga para pengembang sudah memakai solusi yang lebih aneh lagi, dan itu juga menambah masalah yang sama-sama tidak masuk akal. Kalau ketidaknyamanan berupa import level fungsi yang berulang-ulang dan tersembunyi saja bisa dihilangkan, itu sudah kemajuan besar. Lagi pula, ini diusulkan hanya sebagai fitur bahasa yang opsional
Risiko ini bisa cukup dikurangi dengan pengujian otomatis, dan menurut saya sepadan dengan keuntungan startup yang lebih cepat. Waktu startup sama sekali bukan sekadar masalah "kosmetik". Saya pernah menghadapi ini pada monolit Django, di mana hanya karena beberapa library berat, setiap management command, test, dan reload container harus menunggu 10–15 detik. Setelah ditunda dengan lazy import, perbedaannya sangat besar
Kami cenderung menyukai import eksplisit di bagian paling atas, karena itu memunculkan masalah dependensi sejak program dijalankan. Kalau memakai lazy import, ketidaknyamanannya adalah masalah baru ketahuan saat jalur kode tertentu dieksekusi (mungkin beberapa jam atau beberapa hari kemudian)
Sebagian besar waktunya sebenarnya habis untuk mengimpor lalu membongkar modul vendor yang sama sekali tidak dipakai (misalnya hanya modul terkait Requests saja hampir 100 buah). Setelah ditelusuri, total ada lebih dari 500 modul yang diimpor tanpa perlu
Saya juga tidak tahu kenapa code generator sekarang makin sering menghasilkan local import di dalam fungsi alih-alih import di bagian atas. Saya tidak ingin menganjurkan pola itu, karena membuat dependensi modul lebih sulit dipahami dan meningkatkan risiko munculnya cyclic dependency nanti
Saya belum membaca PEP ini sampai selesai, tetapi saya rasa akan bagus kalau ada validasi dependensi lewat flag command line atau alat eksternal, seperti alat untuk type hint
Saya penasaran siapa sebenarnya yang dimaksud dengan "kami"
Bukankah masalah seperti ini seharusnya ditangani lewat testing?