Python'da İç İçe Listeleri Düzleştirmek

Merhabalar, bu yazımda Python'da neredeyse her geliştiricinin karşılaştığı bir problemi konuşalım: elimizde [[1, 2], [3, 4], [5, 6]] gibi bir liste var ve onu [1, 2, 3, 4, 5, 6] haline getirmek istiyoruz. Basit gibi duruyor, gerçekten de basit. Ama 'hangi yolu seçersem hem okunaklı hem hızlı olur' sorusunun birkaç cevabı var ve bazıları sandığınızdan pahalıya patlıyor.

En klasik yol: list comprehension

Tek seviye iç içe geçmiş bir liste için en pythonic ve okunaklı yöntem genellikle nested list comprehension. Şu şekilde yazılıyor:

nested = [[1, 2, 3], [4, 5], [6, 7, 8, 9]]

flat = [item for sublist in nested for item in sublist]
print(flat)  # [1, 2, 3, 4, 5, 6, 7, 8, 9]

Burada for'ların sırası, normal iç içe for döngüsündeki sırayla aynı: önce dış döngü (sublist in nested), sonra iç döngü (item in sublist). Yani aslında şu kodun sıkıştırılmış hali:

flat = []
for sublist in nested:
    for item in sublist:
        flat.append(item)

Bence küçük ve orta boyutlu listelerde varsayılan tercih bu olmalı. Hem hızlı, hem de bir-iki ay sonra koda dönen birinin kafa yormadan anlayacağı kadar net.

itertools.chain.from_iterable: büyük listelerin dostu

İç içe listeniz büyüdükçe itertools.chain.from_iterable daha iyi bir tercih oluyor. Tam olarak bu iş için tasarlanmış bir araç ve C tarafında çalıştığı için comprehension'a kıyasla biraz daha hızlı:

from itertools import chain

nested = [[1, 2, 3], [4, 5], [6, 7, 8, 9]]

flat = list(chain.from_iterable(nested))
print(flat)  # [1, 2, 3, 4, 5, 6, 7, 8, 9]

Alt listeleriniz ayrı değişkenlerde duruyorsa chain fonksiyonunu doğrudan çağırabilirsiniz: chain(list1, list2, list3). Bence bir API'den sayfalama (pagination) ile gelen sonuçları birleştirirken bu yöntem ideal.

sum() ile tek satırlık tuzak

Internette sık karşılaşacağınız 'şık' bir numara da var:

nested = [[1, 2, 3], [4, 5], [6, 7, 8, 9]]
flat = sum(nested, [])

Sonuç doğru. Ama yazımda asıl uyarmak istediğim nokta burası: bu yöntemden büyük listelerde uzak durun. Çünkü sum, her adımda yeni bir liste yaratıp eski sonucu ona kopyalıyor. Yani 1000 alt listeniz varsa, toplamda kabaca O(n²) iş yapıyorsunuz. Aynı şey functools.reduce(operator.concat, nested) için de geçerli; o da aynı tuzağa düşüyor. Küçük scriptler ve hızlı bir REPL denemesi için tamam, üretim koduna girmesin.

Derin düzleştirme: özyinelemeli çözüm

Yukarıdaki yöntemlerin hepsi sadece bir seviye düzleştiriyor. Peki ya elinizde [1, [2, [3, [4, [5]]]]] gibi bilinmeyen derinlikte bir yapı varsa? O zaman özyinelemeye (recursion) başvuracağız:

def flatten_deep(nested):
    result = []
    for item in nested:
        if isinstance(item, list):
            result.extend(flatten_deep(item))
        else:
            result.append(item)
    return result

mixed = [[1, 2], [3, [4, 5]], 6, [[7, 8], [9]]]
print(flatten_deep(mixed))  # [1, 2, 3, 4, 5, 6, 7, 8, 9]

Çok büyük yapılarda araya bir generator sokmak bellek dostudur:

def flatten_gen(nested):
    for item in nested:
        if isinstance(item, list):
            yield from flatten_gen(item)
        else:
            yield item

Tuple, set gibi başka iterable'ları da dahil etmek isterseniz collections.abc.Iterable ile kontrol edin; ama str ve bytes'ı dışarıda bırakmayı unutmayın, yoksa string'ler tek tek karakterlere açılır.

NumPy alternatifi

Veriniz tamamen sayısalsa ve zaten NumPy kullanıyorsanız, flatten veya ravel çok hızlı:

import numpy as np

arr = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
flat = arr.flatten().tolist()

Alt listelerin uzunlukları eşitse np.array direkt çalışır; eşit değilse np.hstack ile düzleştirebilirsiniz. Pandas tarafında Series.explode() benzer bir iş görür.

Sık karşılaşılan hatalar

  • sum(nested, []) ile büyük liste düzleştirmek: Küçük örneklerde sorunsuz görünür, ama eleman sayısı arttıkça O(n²) davranışı yüzünden uygulamanız fark etmeden yavaşlar. Profiling yapmadan bu satırı production'a koymayın.
  • String'i iterable diye saymak: Generic bir flatten_any yazıyorsanız 'merhaba' string'i de iterable olduğu için karakterlere ayrılır. str ve bytes'ı her zaman ignore listesine ekleyin.
  • Eşit olmayan alt listelerde np.array kullanmak: NumPy bu durumda dtype object üretir veya hata verir. Eşit uzunluk garantisi yoksa np.hstack ya da Python tarafındaki yöntemler daha güvenli.
  • Çok derin yapılarda saf rekürsiyon: Python'un default recursion limit'i 1000 civarı. Binlerce seviye iç içe yapıda RecursionError alırsınız; iteratif bir stack ile yazmak gerekir.

Kapanış

Çoğu zaman list comprehension yeter ve okunaklılık adına onu tercih ederim; performansa ihtiyaç duyduğum anda itertools.chain.from_iterable'a geçerim. Derin yapılar için kısa bir rekürsif fonksiyon her projemde duruyor zaten, sizin de cebinizde durmasını öneririm. Umarım faydalı olur, bir sonraki yazıda görüşmek üzere.