dersblog

Python Sözlük (Dictionary) Veri Yapısı

Python sözlük yapısı hafızada anahtar bazlı veriye direk erişim (dizi / vektör / sayı indisli liste erişimi kadar hızlı) sağlar. Basit anahtar - değer ikilisini depolayabiliriz, fakat değer olarak depolanan şey herhangi bir obje olabilir. Bu özellik sözlüklere geniş kullanım alanı sağlar.

En basit kullanım,

# bos sozluk yarat
dict1 = {}

# depola
dict1['anahtar1'] = "deger1"

# erisim yap
dict1['anahtar1']
Out[1]: 'deger1'

Herhangi bir obje depolanır demiştik,

dict1['anahtar2'] = np.array([1,2,3])
dict1['anahtar2']
Out[1]: array([1, 2, 3])

Tüm anahtarları göstermek için

dict1.keys()
Out[1]: dict_keys(['anahtar1', 'anahtar2'])

Bu liste gezilebilir, ve bu sırada anahtar kullanılıp değer ekrana basılabilir,

for k in dict1: print (dict1[k])
deger1
[1 2 3]

Eğer olmayan değere erişirsek hata gelir,

dict1['olmayan anahtar']
KeyErrorTraceback (most recent call last)
<ipython-input-1-90fef2a85d10> in <module>
      1 plt.figure();
      2 
----> 3 dict1['olmayan anahtar']

KeyError: 'olmayan anahtar'

Hata oluşturmadan eğer anahtar varsa değer yoksa None gelsin istersek .get çağrısı var,

print (dict1.get('olmayan anahtar'))
None
print (dict1.get('anahtar1'))
deger1

Kavrama (comprehension) yani [1] tek satırda hem başka bir listeyi gezip aynı anda başka bir liste / sözlük yaratma kabiliyeti sözlükler için de geçerli,

dict2 = { i:x for i,x in enumerate(['ali','veli','ahmet']) }
print (dict2)
{0: 'ali', 1: 'veli', 2: 'ahmet'}

i:x ile anahtar/değer ikilisini başka bir listeyi gezerken tanımladık, ve bu tanımlar toparlanıp bir yeni sözlük yaratımı için kullanıldı.

Sözlük İç Yapısı

Sözlüklerin iç kodlanması nasıl yapılmıştır acaba? Bundan da bahsedelim, mülakat sorusu olabilir, zaten görünmese de bu tür detayları bilmek faydalıdır. Bir sözlük paketini kodlamak için en baz depolama veri yapısı olarak bir dizi (array) kullanabiliriz. Ama dizi erişimi tam sayı bazlıdır, üstelik baştan kaç öğe olduğu bellidir, anahtarı herhangi bir şey olabilecek sözlükleri bunun üstüne nasıl monte edeceğiz?

Böleç (hash) kavramı burada yardıma yetişir. Harfler, alfanumerik herhangi bir veriyi hash ile sayısal forma çevirebileceğimizi biliyoruz,

hash("anahtar1")
Out[1]: 6762465535002540417
hash("anahtar2")
Out[1]: 1439585439643666777

Eğer sözlüğün deposu olarak 10 büyüklüğünde bir listeyi depolama için kullanmak istiyorsak üstteki sayıların 10 üzerinden modülüs hesabını yaparız,

hash("anahtar1") % 10
Out[1]: 7

İşte bu sayı dizide erişim için kullanılır.

Eğer dizi büyüklüğüne kıyasla çok fazla anahtar değeri var ise birden fazla anahtar aynı dizi öğesine düşebilir, bu "çakışma (collision)" durumu yaratır, bu durumda çakışma olan öğede bir liste yaratırız (dizi içinde liste), ve erişim için arama önce anahtar bazlı direk sonra liste bazlı teker teker gider.

DiskDict

Paketten çıkan hali ile Python sözlükleri hafıza bazlıdır. Fakat eğer anahtar erişimi bizi bir disk deposuna götürsün istersek? Dikkat tüm işlemler hafızada yapıldıktan sonra işi bitmiş sözlüğü diske yazmaktan bahsetmiyorum, her anahtar erişimi, her depolama işleme tekil bazlı olarak diske gitmeli diyorum.

Bu ihtiyaç için bazı paketler var. Çoğu dış arayüzünü Python basit sözlük ile benzer tutmuş böylece kodlayan yeni bir erişim stili öğrenmeye mecbur değil, tüm bildik sözlük işlemleri geçerli.

Böyle bir paket diskdict [3]. Kurmak için kaynaklar kullanılırsa iyi olur, önce alttakiler,

sudo apt install python3-leveldb libleveldb-dev libffi-dev

pip install repoze.lru structlog pycparser

Ardından setup.py ile kurulum yapılır. Örnek,

from diskdict import DiskDict

dict3 = DiskDict('/tmp/diskdict')

dict3['anahtar3'] = 'deger3'

Bu işlemler sonrası /tmp/diskdict dizi altında bazı dosyaların yaratılmış olduğunu göreceğiz. Sözlüğün disksel erişim işlemleri bu dosyalar üzerinden yapılıyor, her erişim her depolama direk bu dosyalara gidiyor (dosyaların ikisel formatı muhakkak hızlı erişim için ayarlanmış halde).

Diğer Disk Bazlı Seçenekler

Eğer disk bazlı sözlük erişimini ayrı bir servise ayırıp ona REST APİ bazlı erişim sağlasak bir nevi anahtar/değer veri tabanı (key-value store) kavramına erişmiş olurduk. MongoDB gibi yazılımlar bir bakıma bu servisi sağlıyorlar, daha yüksek ölçekte ek bazı servislerle beraber fakat sonuçta yaptıkları disk bazlı bir sözlük servisi sağlamaktır.

Konu hakkında önceki bir yazı şurada [2].

SQliteDict

Ustteki diskdict kurulumu problem cikartirsa erisimi bir o kadar hizli sqlitedict paketi olabilir. Bu paket ile

from sqlitedict import SqliteDict

dd = SqliteDict('walkdict.sqlite', autocommit=False)

ile bir disk bazlı, daha doğrusu sqlite taban bazlı bir sözlük yaratıyoruz, ardından dd['anahtar'] = 'değer' ve dd.commit() türündeki çağrılar diskteki sözlüğü güncelliyor. Erişim için SqliteDict objesi aynı dosya ismi için tekrar oluşturuluyor ve dd[anahtar'] bize gerekli değeri geri veriyor. Arka planda paket anahtarları muhakkak SQL tablosunda bir ana anahtar olarak kullammış, erişimim hızlı indisler üzerinden ve her şeyi bellege taşımadan yapacaktır.

Olağan Değerler, Sıralanma

Olağan Değerler

Pek çok ihtiyaca yardım etmeye uğraşan ek sözlük tipleri de vardır. Bunlardan biri olmayan bir değere erişildiğinde o anahtar için olağan (default) bir değeri otomatik yaratan bir sözlük tipidir, defaultdict

from collections import defaultdict

dict4 = defaultdict(int)
dict4['anahtar4']
Out[1]: 0

Olağan değer sözlüklerinin en kullanışlı olduğu yer belli anahtar değerlerine tekabül eden listelere öğe eklemek istediğimiz zamandır. Pek çok problemde belli anahtarlara tekabül eden liste değerleri gezeriz, onları bir listeye döngü içinde eklemek isteriz fakat her anahtar için "eğer bu anahtar varsa listeye ekle, yoksa boş liste yarat, ondan sonra ekle" mantığı kod fazlalığı oluşturabilir, defaultdict(list) kullanırsak her anahtarın olağan değeri boş liste olacağı için bir anahtar var mı yok mu bakmadan direk o anahtarın listesine ekleme yapabiliriz çünkü o anahtarın listesi yoksa nasıl olsa yaratılacaktır. Örnek,

from collections import defaultdict

city_list = [('TX','Austin'), ('TX','Houston'), ('NY','Albany'),
('NY', 'Syracuse'), ('NY', 'Buffalo'), ('NY', 'Rochester'), ('TX',
'Dallas'), ('CA','Sacramento'), ('CA', 'Palo Alto'), ('GA',
'Atlanta')]

cities_by_state = defaultdict(list)
for state, city in city_list:
    cities_by_state[state].append(city)

cities_by_state
Out[1]: 
defaultdict(list,
            {'TX': ['Austin', 'Houston', 'Dallas'],
             'NY': ['Albany', 'Syracuse', 'Buffalo', 'Rochester'],
             'CA': ['Sacramento', 'Palo Alto'],
             'GA': ['Atlanta']})

Gördüğümüz gibi cities_by_state[state] üzerinde direk .append yapabildik çünkü listenin orada olacağını varsayabiliyoruz.

Eklenme Sırasını Hatırlayan Sözlük

Anahtarın eklenme sırasını hatırlayıp değerler istendiğinde ona göre listeyi döndüren bir diğer sözlük tipi collections.OrderedDict.

import collections

dict5 = collections.OrderedDict()
dict5["A"]=1
dict5["B"]=2
dict5["D"]=4
dict5["C"]=3
for i in dict5.items(): print(i)
('A', 1)
('B', 2)
('D', 4)
('C', 3)

Sonuca bakıyoruz, liste aynen eklenme sırasını yansıtıyor. Normal bir sözlük bu sonucu garantilemez, sıralama rasgele olabilirdi.

Anahtar Çeşitleri

"Depolanan şey herhangi bir obje olabilir" dedik, buna ek olarak, belli şartlara uymak kaydıyla anahtar çoğu baz Python objeleri bile olabilir. Mesela tüpler (tuples),

tuple1 = ((2,3),(4,5))
tuple2 = ((8,1),(9,9))
dict6 = {}
dict6[tuple1] = "bazi degerler burada"
dict6[tuple2] = "baska degerler"
print (dict6[tuple1])
print (dict6[tuple2])
bazi degerler burada
baska degerler

Anahtar olabilecek temel tipler değiştirilemeyen (immutable) nesnelerdir, bunlar String, int, tuple tipleri. Fakat daha önce bahsettiğimiz gibi bir anahtar sonuçta böleçlenen bir şey, o zaman bir böleç fonksiyonu sağlayan her nesne, kullanıcı tanımlı bile olsa anahtar olabilir.

class C:
    def __init__(self, a1, a2):
        self.a1 = a1
        self.a2 = a2

    def __hash__(self):
        return hash((self.a1, self.a2))

    def __eq__(self, other):
        return (self.a1, self.a2) == (other.a1, other.a2)

object_a = C("a", 1)
object_b = C("a", 1)
object_c = C("b", 2)

a_dictionary = {object_a : 3, object_b : 4, object_c : 5}

print(a_dictionary[object_a])
4

Görüldüğü gibi object_a objesinin tamamını bir anahtar olarak kullandık ve doğru değere erisebildik çünkü sınıf üzerinde __hash__ tanımlandı perde arkasında Python sözlük kodu bu fonksiyonu çağırarak gerekli değeri oradan alabiliyor.

Dikkat edersek object_b aynı değerleri taşıdığı için aynı böleci üretir, o zaman sözlük açısından aynı anahtar değeridir. object_c farklı değerleri olduğu için farklı bir anahtardır.

print(a_dictionary[object_b])
print(a_dictionary[object_c])
4
5

Değer 4 iki üstteki ile aynı, ama 5 farklı.

Kaynaklar

[1] Python Liste Kavraması (List Comprehension)

[2] NoSQL

[3] https://github.com/deep-compute/diskdict


Yukarı