Modul itu cuma file Python yang bisa di-import oleh file lain, supaya isinya bisa dipakai ulang tanpa disalin. File .py apa pun yang sudah pernah kamu tulis sudah termasuk modul. Kamu memindahkan laporan ke report.py, dan file kedua butuh satu fungsi dari situ. Jadi kamu menaruh import report di bagian atas lalu menjalankan file kedua itu.
Import-nya menjalankan seluruh laporan. Ia membaca inputmu dulu, lalu mencetak setiap baris dan totalnya, semuanya sebelum file kedua sampai ke baris berikutnya. Tidak ada error yang muncul, jadi tidak ada yang memberi tahu kenapa.
Sebabnya satu aturan tentang apa yang dilakukan import, dan perbaikannya satu baris plus sedikit penataan ulang. Kamu akan melihat keduanya gagal lalu lulus.
Poin Penting
import reportmenjalankan seluruh file dari atas ke bawah saat itu juga, jadi pekerjaan di level atas terjadi lagi di dalam file yang meng-import-nya.__name__bernilai__main__kalau kamu menjalankan filenya sendiri, dan bernilai nama modul kalau ada yang meng-import; guard membandingkan dua string itu.- Pekerjaan sebaiknya ada di dalam fungsi, karena
defcuma mengikat nama, dan pemanggilan di bawahif __name__ == "__main__":yang memulainya. - Nilai yang dihitung di level modul selesai sekali saja:
totaldi versi awal adalah angka 9, jadi memanggilnya memunculkanTypeError: 'int' object is not callable. - Guard hanya melindungi baris yang menjorok di bawahnya, jadi
printliar di level atas tetap jalan setiap kali file di-import.
Dua file, satu perintah
Aturannya sama seperti di artikel-artikel sebelumnya, misalnya Python Dictionaries: the Key That Isn't There. Kamu menulis programnya, artikelnya memberimu checker, dan kamu menjalankan checker itu sampai berubah dari merah ke hijau. Kamu butuh dua file dan python3. Tidak ada repository, tidak ada Docker, tidak ada akun.
Simpan ini sebagai report.py. Isinya skripmu apa adanya, dengan laporan di level atas:
# report.py
import sys
rows = []
for line in sys.stdin:
line = line.strip()
if not line:
continue
name, qty = line.split(",")
rows.append((name, int(qty)))
total = sum(qty for _, qty in rows)
print("item,qty")
for name, qty in rows:
print(f"{name},{qty}")
print(f"total,{total}")
Apa yang dilakukan import pada sebuah file
Nama file adalah nama yang kamu import: report.py di-import sebagai report, tanpa .py.
Waktu Python menemui import report, ia mencari filenya lalu menjalankannya dari atas ke bawah, saat itu juga. Ini terjadi sekali per proses. Setiap baris di level atas dijalankan: loop atas sys.stdin, sum, tiap print. Python tidak tahu baris mana yang kamu maksud sebagai bagian yang bisa dipakai ulang.
Dari mana ia mencari filenya? Folder skrip yang kamu jalankan ada paling depan di jalur import. Saya cek dengan probe satu baris di folder bernama probe. Saat menjalankan python3 path_probe.py di situ, sys.path[0] berisi basename probe. Itu sebabnya dua file dalam satu folder bisa saling import tanpa setup apa pun.
Sekarang versi awalnya, di-import. Ia mencetak laporan lengkap: item,qty, kopi,3, teh,2, kopi,4, total,9. Ada masalah kedua juga. total adalah angka yang sudah jadi, bukan fungsi, jadi memanggilnya memunculkan TypeError: 'int' object is not callable.
Simpan checker-nya di sebelah report.py. Ia memberi program itu kopi,3, teh,2, dan kopi,4, dan mengharapkan total 9. Ia menjalankan filemu sekali sebagai skrip dan dua kali sebagai import.
# check_report.py
import subprocess
import sys
DATA = """kopi,3
teh,2
kopi,4
"""
EXPECTED = """item,qty
kopi,3
teh,2
kopi,4
total,9"""
def run(args, stdin=""):
return subprocess.run(
[sys.executable] + args,
input=stdin,
text=True,
capture_output=True,
)
def report(results):
for label, passed, detail in results:
mark = "PASS" if passed else "FAIL"
note = f" <- {detail}" if detail and not passed else ""
print(f"{mark} {label}{note}")
passed = [ok for _, ok, _ in results].count(True)
print(f"\n{passed} of {len(results)} checks passed")
def main():
as_script = run(["report.py"], DATA)
as_import = run(["-c", "import report"], DATA)
as_call = run(["-c", "import report; print(report.total([('a', 1), ('b', 2)]))"])
stderr_script = as_script.stderr.strip().splitlines()
results = [
("the script runs without crashing", as_script.returncode == 0,
stderr_script[-1] if stderr_script else ""),
("the script prints the report, total last", as_script.stdout.strip() == EXPECTED,
repr(as_script.stdout)),
("importing it does not crash", as_import.returncode == 0, as_import.stderr.strip()),
("importing it prints nothing", as_import.stdout == "", repr(as_import.stdout)),
("total() is a function you can call", as_call.returncode == 0 and as_call.stdout.strip() == "3",
as_call.stderr.strip().splitlines()[-1] if as_call.stderr.strip() else repr(as_call.stdout)),
]
report(results)
return 0 if all(ok for _, ok, _ in results) else 1
if __name__ == "__main__":
sys.exit(main())
Jalankan:
python3 check_report.py
PASS the script runs without crashing
PASS the script prints the report, total last
PASS importing it does not crash
FAIL importing it prints nothing <- 'item,qty\nkopi,3\nteh,2\nkopi,4\ntotal,9\n'
FAIL total() is a function you can call <- TypeError: 'int' object is not callable
3 of 5 checks passed
Exit code-nya 1. Kalau dijalankan sebagai skrip, filenya baik-baik saja. Kedua kegagalan datang dari import, dan yang pertama menunjukkan seluruh laporan yang bocor keluar.
Satu baris menentukan pekerjaan file itu
Python memberi setiap file sebuah nama selama ia berjalan. Nama itu ada di __name__. Isinya __main__ kalau kamu menjalankan filenya sendiri, dan nama modul kalau ada yang meng-import-nya.
Taruh satu baris, print(__name__), di file bernama name_probe.py. Jalankan python3 name_probe.py dan ia mencetak __main__. Import dari file lain, dan baris yang sama mencetak name_probe.
Kodenya identik dan namanya berbeda. Baris if __name__ == "__main__": membandingkan dua string itu. Kalau cocok, blok yang menjorok dijalankan. Kalau file lain yang meng-import punyamu, keduanya tidak cocok dan blok itu dilewati.
Jadi rencananya punya dua bagian. Taruh pekerjaan di dalam fungsi, karena def cuma mengikat nama saat file di-import. Lalu taruh satu pemanggilan yang memulai pekerjaan itu di bawah guard.
Perbaikannya
Saya pecah versi awal jadi tiga fungsi. parse membaca barisnya, total menjumlahkannya, dan main mencetak laporan. Sekarang total adalah fungsi, dan itu menghapus TypeError tadi. Dua baris terakhir memegang guard-nya.
# report.py
import sys
def parse(lines):
rows = []
for line in lines:
line = line.strip()
if not line:
continue
name, qty = line.split(",")
rows.append((name, int(qty)))
return rows
def total(rows):
return sum(qty for _, qty in rows)
def main():
rows = parse(sys.stdin)
print("item,qty")
for name, qty in rows:
print(f"{name},{qty}")
print(f"total,{total(rows)}")
if __name__ == "__main__":
main()
Jalankan checker-nya lagi:
python3 check_report.py
PASS the script runs without crashing
PASS the script prints the report, total last
PASS importing it does not crash
PASS importing it prints nothing
PASS total() is a function you can call
5 of 5 checks passed
Exit code-nya 0.
Yang tidak dilakukan ini
Package adalah folder dengan file __init__.py, dan relative import adalah bentuk from . import yang dipakai di dalamnya. Keduanya untuk nanti. Dua file dalam satu folder tidak butuh keduanya.
Guard juga tidak menyelamatkan print yang kamu tinggalkan di level modul. Kalau ada print("debug") liar di luar semua fungsi dan di luar guard, setiap proses yang meng-import file itu mencetaknya sekali. Guard hanya melindungi baris yang menjorok di bawahnya.
Sebelum artikel berikutnya
Hapus dua baris terakhir dari file yang sudah diperbaiki: baris if __name__ == "__main__": dan pemanggilan main() di bawahnya. Jalankan checker-nya lagi. Saya ukur 4 dari 5 dan exit code 1, dan satu-satunya kegagalan adalah ini:
FAIL the script prints the report, total last <- ''
Skripnya jalan dan tidak mencetak apa pun. Sekarang kembalikan kedua baris itu dan jalankan checker sampai ia bilang 5 dari 5 lagi.
Itu membuktikan checker bisa membedakan file yang dijalankan dan yang cuma di-import. Ia tidak bisa membuktikan kode di dalam main() benar.
Kalau import menjalankan seluruh file, apa lagi di skrip lamamu yang selama ini sudah berjalan di level atas?