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_anyyazıyorsanız'merhaba'string'i de iterable olduğu için karakterlere ayrılır.strvebytes'ı her zaman ignore listesine ekleyin. - Eşit olmayan alt listelerde
np.arraykullanmak: NumPy bu durumda dtypeobjectüretir veya hata verir. Eşit uzunluk garantisi yoksanp.hstackya 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
RecursionErroralı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.
