Data sementara sering memiliki profil ukuran yang sulit diprediksi. Sebagian besar request mungkin hanya menghasilkan beberapa kilobyte, sementara sesekali proses import, report, archive, atau upload dapat tumbuh hingga ratusan megabyte.
Menggunakan io.BytesIO nyaman untuk kasus kecil, tetapi seluruh isinya tetap berada di memori. Menggunakan temporary file menghindari penyimpanan seluruh payload di memori, tetapi setiap payload memakai storage berbasis filesystem meskipun ukurannya sangat kecil.
tempfile.SpooledTemporaryFile dari Python menawarkan jalan tengah. Objek ini berperilaku seperti file sambil menyimpan data di memori hingga threshold yang dikonfigurasi. Ketika data melewati threshold tersebut, data di-roll over ke temporary file dan operasi berlanjut melalui interface yang sama.
Model mental yang berguna adalah satu file-like object yang seekable dengan dua kemungkinan fase storage. Kode Anda sebaiknya bergantung pada interface file, bukan pada fase storage yang sedang aktif.
Mulai dengan buffer file-like yang dapat berpindah ke disk
Misalkan fungsi export menulis byte dalam beberapa chunk dan fungsi lain mengharapkan file-like object:
from tempfile import SpooledTemporaryFile
def build_export(chunks):
spool = SpooledTemporaryFile(max_size=1_000_000, mode="w+b")
for chunk in chunks:
spool.write(chunk)
spool.seek(0)
return spoolPemanggil dapat membaca hasilnya sama seperti membaca file binary biasa:
with build_export([b"header\n", b"row-1\n", b"row-2\n"]) as export:
data = export.read()
print(data)Untuk data yang tetap pada atau di bawah threshold ukuran yang dikonfigurasi, objek dapat tetap berbasis memori. Jika ukurannya melampaui max_size, SpooledTemporaryFile menulis isi ke temporary file dan terus beroperasi sebagai file-like object.
Pemanggil tidak memerlukan jalur kode terpisah untuk kedua kasus tersebut.
Contoh secara eksplisit memakai mode="w+b" karena payload berupa byte. Untuk teks, gunakan text mode seperti "w+" dan tentukan encoding bila diperlukan.
Threshold adalah kebijakan rollover, bukan batas payload
max_size mengendalikan kapan rollover berbasis ukuran terjadi. Parameter ini tidak menolak data yang lebih besar.
Perbedaan ini penting:
with SpooledTemporaryFile(max_size=16, mode="w+b") as spool:
spool.write(b"0123456789")
spool.write(b"abcdefghij")
spool.seek(0)
payload = spool.read()
assert payload == b"0123456789abcdefghij"Payload berukuran 20 byte sehingga melampaui threshold 16 byte. Objek tetap dapat digunakan; storage-nya hanya berpindah ke temporary file.
Hal ini membuat SpooledTemporaryFile berguna ketika Anda ingin mengoptimalkan kasus kecil yang umum sambil tetap menerima input lebih besar.
Mekanisme ini bukan perlindungan terhadap input tanpa batas. Client yang dapat mengirim data tanpa batas tetap dapat menghabiskan ruang disk setelah rollover. Jika ukuran input harus dibatasi, terapkan limit terpisah pada level aplikasi saat membaca.
Rollover mempertahankan posisi dan isi file
Kode yang menggunakan spooled file tidak perlu memulai ulang ketika storage berubah.
Pertimbangkan writer yang melewati threshold di tengah rangkaian penulisan:
with SpooledTemporaryFile(max_size=8, mode="w+b") as spool:
spool.write(b"hello")
spool.write(b" world")
spool.seek(0)
assert spool.read() == b"hello world"Write kedua membuat total data lebih besar dari delapan byte. Implementasi memindahkan data ke temporary file, tetapi file logis tetap berisi byte yang telah ditulis sebelumnya.
Kontinuitas ini merupakan alasan utama memilih abstraksi tersebut dibanding berpindah manual dari BytesIO ke temporary file. Desain manual harus menyalin data yang sudah ada, mempertahankan offset saat ini, memindahkan ownership, dan memastikan cleanup. SpooledTemporaryFile memusatkan transisi tersebut.
Seeking tetap penting ketika berpindah dari menulis ke membaca
Rollover tidak mengubah aturan posisi file normal.
Setelah menulis, posisi saat ini berada di akhir data. Memanggil read() secara langsung akan menghasilkan byte string kosong:
with SpooledTemporaryFile(max_size=1024, mode="w+b") as spool:
spool.write(b"report data")
assert spool.read() == b""Pindahkan posisi sebelum membaca:
with SpooledTemporaryFile(max_size=1024, mode="w+b") as spool:
spool.write(b"report data")
spool.seek(0)
assert spool.read() == b"report data"Kesalahan ini mudah disangka sebagai masalah rollover karena objek yang sama menangani pembacaan dan penulisan. Sebenarnya ini hanya perilaku file seekable biasa.
Jika beberapa tahap berbagi objek yang sama, dokumentasikan siapa yang bertanggung jawab mengatur posisinya. Konvensi yang berguna adalah producer mengembalikan file pada posisi awal ketika tahap berikutnya diharapkan membacanya.
Gunakan interface file, bukan memeriksa internal storage
Spooled file menyediakan operasi yang biasanya diperlukan consumer: read(), write(), seek(), tell(), truncate(), iterasi, serta cleanup melalui context manager.
Kode umumnya tidak perlu memeriksa atribut private untuk mengetahui apakah rollover sudah terjadi:
# Hindari mengikat logika aplikasi pada state implementasi private.
if spool._rolled:
...Atribut yang diawali underscore bukan bagian dari kontrak public API. Logika yang bergantung padanya juga dapat merusak abstraksi: kode downstream kini peduli terhadap detail storage yang seharusnya dapat dipertukarkan.
Lebih baik gunakan kode berbasis capability:
def checksum_input(file_obj):
file_obj.seek(0)
total = 0
while chunk := file_obj.read(64 * 1024):
total = (total + sum(chunk)) % 2**32
return totalFungsi ini bekerja baik saat spool berbasis memori maupun temporary file. Fungsi yang sama juga dapat bekerja dengan file object binary lain yang kompatibel sehingga testing dan reuse lebih mudah.
Beberapa operasi memaksa rollover meskipun di bawah threshold
Ukuran bukan satu-satunya pemicu.
Method rollover() yang terdokumentasi secara eksplisit memindahkan data ke temporary file:
with SpooledTemporaryFile(max_size=1_000_000, mode="w+b") as spool:
spool.write(b"small payload")
spool.rollover()
spool.seek(0)
assert spool.read() == b"small payload"Memanggil fileno() juga menyebabkan rollover karena buffer di memori tidak menyediakan file descriptor sistem operasi yang dijanjikan fileno():
with SpooledTemporaryFile(max_size=1_000_000, mode="w+b") as spool:
spool.write(b"small payload")
fd = spool.fileno()
print(fd)Ini merupakan batas penting ketika berintegrasi dengan API level lebih rendah. Library yang memanggil fileno() dapat mengubah spool kecil berbasis memori menjadi temporary file nyata meskipun threshold ukuran tidak pernah dilewati.
Jika mempertahankan payload kecil di memori penting bagi workload Anda, periksa apakah komponen downstream membutuhkan file descriptor.
Pola realistis: staging data sebelum menyerahkannya
Spooling berguna ketika producer menghasilkan data secara inkremental tetapi consumer menginginkan file object yang dapat dibaca.
Misalnya, bayangkan membuat export CSV sebelum mengirimkannya ke layer lain:
from tempfile import SpooledTemporaryFile
def create_csv(rows):
spool = SpooledTemporaryFile(
max_size=8 * 1024 * 1024,
mode="w+",
encoding="utf-8",
newline="",
)
spool.write("id,name\n")
for user_id, name in rows:
spool.write(f"{user_id},{name}\n")
spool.seek(0)
return spoolThreshold delapan megabyte adalah pilihan aplikasi, bukan rekomendasi universal. Nilainya perlu mencerminkan ukuran payload yang diharapkan, concurrency, memori tersedia, kapasitas temporary storage, serta profil biaya sistem di sekitarnya.
Jika 200 request dapat membangun export secara bersamaan, threshold delapan megabyte menghasilkan exposure memori worst-case yang sangat berbeda dibanding threshold yang sama pada command-line tool untuk satu pengguna.
Karena itu, max_size sebaiknya dipilih berdasarkan constraint workload, bukan disalin dari contoh.
Spooling dapat mengurangi tekanan memori, tetapi tidak menjamin batas RAM
Mudah untuk menggambarkan threshold spool sebagai “gunakan RAM paling banyak sebesar ini.” Pernyataan itu terlalu kuat.
Threshold mengatur kapan file object ini memindahkan isi yang disimpannya ke temporary file. Proses Anda mungkin masih menyimpan salinan lain dari data yang sama. Contohnya:
chunk = receive_large_chunk()
spool.write(chunk)chunk tetap ada sampai kode melepaskannya, terlepas dari tempat spool menyimpan salinannya sendiri.
Temporary buffer, parser object, representasi terkompresi, request body, dan salinan downstream juga dapat menambah penggunaan memori.
Untuk pemrosesan dengan memori terbatas, gabungkan spooling dengan pembacaan inkremental dan ukuran chunk yang wajar:
def copy_stream(source, destination):
while chunk := source.read(64 * 1024):
destination.write(chunk)Meski begitu, total memori proses bergantung pada bagian aplikasi lainnya. Spooling mengendalikan satu bagian data path; mekanisme ini bukan pembatas memori untuk seluruh proses.
Temporary storage memiliki failure mode sendiri
Setelah rollover, operasi write bergantung pada fasilitas temporary file milik sistem.
Artinya, operasi dapat gagal karena lokasi temporary kehabisan ruang, tidak tersedia, terkena quota, atau tidak dapat memenuhi write karena alasan lain. Perlakukan write sebagai operasi I/O yang dapat melempar exception, bukan menganggap rollover selalu berhasil.
Argumen dir dapat memilih direktori untuk temporary-file storage:
with SpooledTemporaryFile(
max_size=4 * 1024 * 1024,
mode="w+b",
dir="/var/tmp",
) as spool:
spool.write(b"temporary data")Pilih direktori tertentu hanya ketika jaminan deployment membuat lokasi tersebut sesuai. Hard-code path Unix membuat kode kurang portabel, dan mengganti direktori tidak menghilangkan kebutuhan untuk merencanakan kapasitas serta perilaku cleanup.
Untuk library portabel, membiarkan tempfile memilih lokasi temporary biasanya merupakan default yang lebih baik.
Cleanup harus eksplisit dan ownership harus jelas
SpooledTemporaryFile dapat digunakan sebagai context manager, sehingga model ownership paling sederhana adalah:
with SpooledTemporaryFile(max_size=1024 * 1024, mode="w+b") as spool:
spool.write(b"temporary work")
# Consume the spool here.Objek ditutup ketika block berakhir, termasuk ketika exception membuat eksekusi keluar dari block.
Mengembalikan spool yang masih terbuka dari sebuah fungsi juga valid, tetapi ownership kemudian berpindah kepada pemanggil:
def make_payload():
spool = SpooledTemporaryFile(max_size=1024 * 1024, mode="w+b")
spool.write(b"payload")
spool.seek(0)
return spool
with make_payload() as payload:
consume(payload)Dokumentasikan tanggung jawab tersebut. Kebocoran temporary-file object saat load tinggi dapat menghabiskan file descriptor dan temporary storage setelah rollover.
Pilih alternatif yang lebih sederhana ketika profil ukuran dapat diprediksi
SpooledTemporaryFile paling berguna ketika ukuran payload cukup bervariasi sehingga perilaku berbasis memori dan file sama-sama bernilai.
Gunakan io.BytesIO ketika data diketahui kecil dan Anda tidak membutuhkan file descriptor nyata. Ini adalah abstraksi in-memory yang lebih sederhana tanpa perilaku rollover.
Gunakan TemporaryFile ketika data diperkirakan besar, ketika storage berbasis disk dapat diterima sejak awal, atau ketika consumer memang selalu membutuhkan fileno().
Untuk data yang tidak seharusnya dimaterialisasi penuh, keduanya bukan pilihan ideal. Pilih streaming dari producer ke consumer agar hanya chunk terbatas yang berada di memori pada satu waktu.
Spooling berada di tengah: berguna ketika Anda membutuhkan semantik file yang seekable, kasus umum berukuran kecil, kasus besar tetap sah, dan perpindahan storage di balik interface lebih baik daripada mempertahankan dua jalur terpisah.
Kesalahan umum yang perlu dihindari
Kesalahan terpenting mengikuti langsung model storage-nya.
Jangan memperlakukan max_size sebagai limit ukuran input; terapkan limit secara terpisah. Jangan berasumsi data tetap di memori hanya karena lebih kecil dari threshold, karena fileno() atau rollover() dapat memaksa pembuatan temporary file. Jangan memeriksa atribut private untuk mengambil keputusan bisnis. Jangan lupa memanggil seek() ketika berpindah dari menulis ke membaca. Dan jangan memilih threshold tanpa mempertimbangkan jumlah spooled object yang dapat hidup secara bersamaan.
Ini bukan detail pinggiran. Hal-hal tersebut menentukan apakah desain spooling benar-benar memperbaiki perilaku resource atau hanya memindahkan tekanan dari memori ke temporary storage.
Kesimpulan
SpooledTemporaryFile adalah tool standard library yang berguna untuk data sementara dengan ukuran tidak pasti. Ia memberi pemanggil satu interface file-like yang seekable, mempertahankan data kecil di memori bila memungkinkan, dan memindahkan data yang lebih besar ke temporary-file storage tanpa memerlukan jalur kode kedua.
Gunakan ketika transisi tersebut sesuai dengan workload Anda. Tentukan threshold dengan sengaja, alirkan data ke dalamnya dalam chunk terbatas, antisipasi kegagalan I/O normal setelah rollover, dan buat kode downstream tetap berfokus pada public file interface, bukan internal storage.
Jika kondisi tersebut tidak berlaku, BytesIO, TemporaryFile, atau streaming sebenarnya mungkin merupakan pilihan yang lebih sederhana.