2 poin oleh GN⁺ 2025-10-04 | 1 komentar | Bagikan ke WhatsApp
  • 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 lazy yang 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.LazyLoader dan paket pihak ketiga lazy_loader memang 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.)

  • lazy hanya 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 (*)
  • 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_CHECKING yang 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 lazy ke 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

 
GN⁺ 2025-10-04
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 diatasi

    • Saya 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/bazel

  • Saya 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 sebagainya

    • Kalau 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 E402

  • Ada yang mengatakan bahwa lazy import semi-otomatis bisa dilakukan lewat kelas LazyLoader. Hanya saja, cara ini memanfaatkan bagian dalam sistem import Python yang cukup tidak jelas, sehingga bahkan penjelasannya di Stack Overflow pun kurang enak dibaca (Q&A terkait). Karena itu saya mencoba sendiri membuat proof of concept yang menjadikan semua import lazy tanpa sintaks eksplisit dari programmer

import sys
import threading  # needed on Python 3.13, at least in the REPL
from importlib.util import LazyLoader  # this one MUST be eager!
class LazyPathFinder(sys.meta_path[-1]):  # subclass _frozen_importlib_external.PathFinder
  @classmethod
  def find_spec(cls, fullname, path=None, target=None):
    base = super().find_spec(fullname, path, target)
    base.loader = LazyLoader(base.loader)
    return base
sys.meta_path[-1] = LazyPathFinder

Dengan 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:

import this  # does not print anything
print(type(this))  # <class 'importlib.util._LazyModule'>
rot13 = this.s  # Prints the Zen and loads the module
print(type(this))  # <class 'module'>

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)

    • Sebaliknya, kalau semua import otomatis di-defer, kecepatan eksekusi pip untuk tugas-tugas singkat akan langsung meningkat
$ time pip install --disable-pip-version-check
ERROR: You must give at least one requirement to install (see "pip help install")

real  0m0.399s
user  0m0.360s
sys   0m0.041s

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?