Python'da Liste İçinden Tekrarları Atmak
Selamlar, bu yazımda Python'da bir liste içindeki tekrar eden elemanları nasıl atacağımıza bakacağız. Konu sığ gibi duruyor ama iş başa düşünce 'sırayı koruyayım mı, hızı mı, yoksa içinde dict olan kayıtları mı temizliyorum' diye birden çok soru çıkıyor. Hadi tek tek üzerinden geçelim.
En kolayı: set()
Bir listeyi set'e çevirip tekrar listeye almak, tekrarları atmanın en kısa yolu. Bence günlük işlerin yüzde sekseninde bu yeter.
sayilar = [1, 2, 2, 3, 4, 4, 4, 5]
benzersiz = list(set(sayilar))
print(benzersiz)
Tek dezavantajı şu: set sırayı korumaz. Yani [3, 1, 2, 1, 3] gibi bir listeyi geçirdiğinizde dönen sıranın orijinalle aynı olacağına dair bir garanti yok. Bir de elemanlarınızın hashlenebilir olması gerekiyor; integer, string, tuple sorun değil ama list veya dict koyamazsınız.
Sırayı korumak: dict.fromkeys()
Python 3.7'den itibaren dict insertion order'ı koruyor. Bu da bize sıra koruyarak tekrar atmanın temiz bir yolunu veriyor.
renkler = ['kirmizi', 'mavi', 'kirmizi', 'yesil', 'mavi', 'sari']
benzersiz = list(dict.fromkeys(renkler))
print(benzersiz)
# ['kirmizi', 'mavi', 'yesil', 'sari']
Burada olan şu: dict.fromkeys her elemanı dict'in anahtarı yapıyor, dict anahtarları benzersiz olmak zorunda olduğu için tekrarlar düşüyor, sıra da ilk görüldükleri sıraya göre kalıyor. Tek satırda hem deduplication hem sıra koruma. Şahsi tercihim, sırayı umursadığım her durumda bu.
Performans karşılaştırması
Aklınıza 'peki maliyeti ne?' sorusu gelmiştir. Hızlı bir benchmark yazalım:
import timeit
veri = list(range(1000)) * 10 # 10000 eleman, çok sayıda tekrar
set_sure = timeit.timeit(lambda: list(set(veri)), number=1000)
dict_sure = timeit.timeit(lambda: list(dict.fromkeys(veri)), number=1000)
print(f'set(): {set_sure:.4f} sn')
print(f'dict.fromkeys: {dict_sure:.4f} sn')
Tipik çıktı şöyle:
set(): 0.2341 sn
dict.fromkeys: 0.2876 sn
set ufak bir farkla önde, ama ikisi de O(n). Yani büyük listelerde bile fark dramatik değil; sıra önemliyse dict.fromkeys'in maliyeti rahatlıkla göze alınır.
Manuel döngü: kontrol size kalsın
Bazen 'şu kritere göre tekrar say' demeniz gerekir. Mesela bir sözlük listesinde sadece id alanına bakarak tekrar atmak isteyebilirsiniz. O zaman manuel döngü:
def tekrarsiz(items, anahtar=None):
gorulenler = set()
sonuc = []
for item in items:
k = anahtar(item) if anahtar else item
if k not in gorulenler:
gorulenler.add(k)
sonuc.append(item)
return sonuc
kullanicilar = [
{'id': 1, 'ad': 'Ayse'},
{'id': 2, 'ad': 'Mehmet'},
{'id': 1, 'ad': 'Ayse (kopya)'},
]
print(tekrarsiz(kullanicilar, anahtar=lambda u: u['id']))
gorulenler setinde sadece anahtarları tutuyoruz, sonuç listesine orijinal nesneyi atıyoruz. Hem hızlı hem okunaklı.
Hashlenemeyen objeler
Liste içinde dict varsa set'e direkt atamazsınız, unhashable type: 'dict' hatası alırsınız. Çözüm, dict'i hashlenebilir bir şeye çevirip karşılaştırmak. Pratikte JSON serileştirmek en sağlam yol:
import json
def dict_listesi_tekrarsiz(kayitlar):
gorulenler = set()
sonuc = []
for d in kayitlar:
anahtar = json.dumps(d, sort_keys=True)
if anahtar not in gorulenler:
gorulenler.add(anahtar)
sonuc.append(d)
return sonuc
sort_keys=True kritik; aynı veriyi iki dict farklı sırada tutuyorsa string temsili de farklı çıkar ve tekrarı kaçırırsınız.
List comprehension - dikkatli kullanın
Tek satırda halletmek isteyenler için klasik bir kalıp var ama tuzaklı:
items = ['elma', 'muz', 'elma', 'kiraz', 'muz']
benzersiz = [x for i, x in enumerate(items) if x not in items[:i]]
Bu çalışır, sırayı da korur. Fakat her eleman için items[:i] dilimini tarıyor; karmaşıklık O(n^2). Yüz elemana kadar farketmez, on bin elemanda fark eder. Bence şirin görünüyor diye buna sarılmayın, dict.fromkeys aynı işi O(n)'de yapıyor.
Pandas tarafı
Zaten DataFrame ile çalışıyorsanız drop_duplicates her şeyi kendisi halleder:
import pandas as pd
kayitlar = [
{'ad': 'Ayse', 'departman': 'Muhendislik'},
{'ad': 'Burak', 'departman': 'Satis'},
{'ad': 'Ayse', 'departman': 'Muhendislik'},
]
df = pd.DataFrame(kayitlar)
print(df.drop_duplicates().to_dict('records'))
Belirli kolonlara göre tekrar atmak istiyorsanız subset parametresi var: df.drop_duplicates(subset=['ad']). Pandas zaten projeye girmişse bu yöntem hem hızlı hem okunaklı; sırf bu iş için pandas çağırmaya değmez tabii.
Sık karşılaşılan hatalar
- set kullanıp sıraya güvenmek:
setPython tarafında sırasız bir veri yapısı, deterministik bir çıktı sırası beklerseniz bir gün şaşırırsınız. Sıra önemliysedict.fromkeysveya manuel döngü. - Sözlük listesinde set denemek:
unhashable typehatasını burada görürsünüz. JSON ile serileştirip karşılaştırma yapın ya dafrozenset(d.items())kullanın. - List comprehension'ı büyük veride kullanmak: O(n^2) maliyet sessizce sürünür. Profiler takmadan farketmezsiniz, sonra 'kod neden yavaş' sorusunu soruverirsiniz.
Kapanış
Özetle, hızlı ve sırasız iş için set, sıra korunsun isteniyorsa dict.fromkeys, özel mantık gerekiyorsa manuel döngü, DataFrame zaten elinizdeyse pandas. Bana sorarsanız dict.fromkeys çoğu durumda en dengeli seçenek; hem hızlı hem sıra korur hem de tek satır. Umarım faydalı olur, bir sonraki yazıda görüşmek üzere.
