Python'da Listeyi Parçalara Bölmek

Merhabalar, bu yazımda Python'da bir listeyi daha küçük parçalara nasıl böldüğümüze bakacağız. Konu kulağa basit geliyor olabilir - aslında bir noktaya kadar öyle de - ama gerçek hayatta toplu veritabanı insert'i, API sayfalama, paralel işleme gibi yerlerde tam olarak hangi yöntemi seçtiğin epey fark yaratıyor. Hadi başlayalım.

Senaryo şu: elinizde 100 bin kayıt var, bunları veritabanına 1000'lik gruplar halinde basmak istiyorsunuz. Ya da bir API rate limit'i var, dakikada en fazla 50 istek geçebiliyor. Ya da concurrent.futures ile paralelleştireceğiniz bir iş var, her worker'a ayrı bir parça vermeniz gerekiyor. Hepsi aynı temel probleme dönüyor: listeyi parçalara böl.

En klasik yol: list comprehension ile slicing

Aklınıza gelen ilk yöntem muhtemelen şu olur, ki bence çoğu zaman da en doğrusu budur:

def parcala(liste, boyut):
    return [liste[i:i + boyut] for i in range(0, len(liste), boyut)]

veri = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print(parcala(veri, 3))
# [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]

Üç satır, okunaklı, sürpriz yok. Son parça eksik kalırsa kalır, kimseyi rahatsız etmez. Liste küçük-orta büyüklükteyse (diyelim 100 bin elemana kadar) bu yaklaşımı bırakıp başka bir şeye geçmek için ciddi bir gerekçeniz olmalı.

Bellek dostu sürüm: generator

Liste milyonlarca elemana çıkıyorsa veya daha kötüsü dosyadan satır satır okuyorsanız, hepsini bir anda parçalara bölüp belleğe almak israf. Generator burada işi çözüyor:

def parcala_gen(liste, boyut):
    for i in range(0, len(liste), boyut):
        yield liste[i:i + boyut]

for parca in parcala_gen(range(1_000_000), 10_000):
    # her seferinde sadece bir parca bellekte
    toplam = sum(parca)

Tek değişiklik return yerine yield. Ama davranış tamamen başka: artık parçalar gerektiği anda üretiliyor, hepsi aynı anda bellekte durmuyor. Bunu özellikle ETL benzeri pipeline'larda sıkça kullanırım.

Python 3.12 ile gelen itertools.batched

Eğer 3.12 veya üstündeyseniz, standart kütüphane bu işi sizin için zaten yapıyor:

from itertools import batched

veri = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
for parca in batched(veri, 3):
    print(parca)
# (1, 2, 3)
# (4, 5, 6)
# (7, 8, 9)
# (10,)

Dikkat: parçalar tuple olarak dönüyor, list değil. Liste lazımsa list(parca) yapacaksınız. Şahsen bu küçük detayı ilk gördüğümde 'ya niye?' demiştim, ama tuple daha hafif ve immutable olduğu için aslında mantıklı bir tercih.

Indekslenemeyen şeyler için: itertools.islice

Elinizdeki şey bir liste değil de bir generator veya iterator ise, liste[i:i+n] çalışmaz - çünkü slicing yok. İşte islice tam buraya:

from itertools import islice

def parcala_iter(iterable, boyut):
    it = iter(iterable)
    while True:
        parca = list(islice(it, boyut))
        if not parca:
            break
        yield parca

Bu fonksiyon hem listeyi yer hem de bir DB cursor'unu, bir generator'ı, bir dosya okuyucuyu... her türlü iterable'ı parçalara böler. 3.12 öncesinde bence en taşınabilir çözüm budur.

NumPy ile sayısal veri

Sayısal verilerle uğraşıyorsanız ve zaten NumPy projedeyse, array_split özellikle 'N eşit parçaya böl' senaryolarında çok temiz:

import numpy as np

dizi = np.arange(10)
for p in np.array_split(dizi, 3):
    print(p)
# [0 1 2 3]
# [4 5 6]
# [7 8 9]

Eleman sayısı parça sayısına tam bölünmediğinde, array_split farkı ilk parçalara dağıtır. np.split ise tam bölünmüyorsa direkt hata verir, dikkat.

Sık karşılaşılan tuzaklar

  • chunk_size=0 veya negatif değer geçmek: range(0, n, 0) ValueError fırlatır, ama negatifte sessizce boş liste döner. Fonksiyonun başında if boyut < 1: raise ValueError(...) koymak ileride saatlerce debug etmekten kurtarır.
  • Generator'ı iki kez tüketmeye çalışmak: Generator bir kez biter ve biter. Üzerinden iki kez geçmeniz gerekiyorsa ya list() ile materialize edin ya da itertools.tee kullanın - ama tee de sonuçta belleğe alıyor, sihir yok.
  • batched çıktısını liste sanmak: Tuple döner. parca.append(...) yazınca AttributeError yer, ekrana bakar 'ne oldu' dersiniz.
  • Çok küçük parçalarla DB'ye basmak: 50 kayıt için her seferinde yeni bir transaction açıyorsanız, asıl darboğaz parçalama değil, transaction overhead'i. Batch boyutunu 500-5000 arasında tutmak çoğu PostgreSQL/MySQL için tatlı nokta.

Hangisini seçmeli?

Tecrübeyle sabittir ki, %80 vakada list comprehension yeter. Liste devasa veya iterable indekslenemiyorsa generator'a (islice) geçin. Python 3.12 elinizdeyse batched standart kütüphane çözümü olarak en temizi. NumPy zaten projedeyse ve sayısal veriyse array_split doğal seçim.

Kapanış

Kısacası, listeyi parçalara bölmek için tek doğru yöntem yok; durum ne istiyorsa o. Bence aklınızda iki ana eksen olsun: bellek mi sıkışık, yoksa kod sadeliği mi öncelik. İkisinden birini seçince geri kalanı kendiliğinden netleşiyor. Umarım faydalı olur, bir sonraki yazıda görüşmek üzere.