Yanıtlar:
İşte Python'un bir araya getirebildiğim dikte ettiği her şey (muhtemelen herkesin bilmek istediklerinden daha fazla; ancak cevap kapsamlı).
dict
, karma çarpışmaları çözmek için açık adresleme kullanır (aşağıda açıklanmıştır) (bkz. Dictobject.c: 296-297 ).O(1)
indekse göre arama yapabilirsiniz ).Aşağıdaki şekil bir Python hash tablosunun mantıksal bir temsilidir. Aşağıdaki şekilde, 0, 1, ..., i, ...
solda , karma tablodaki yuvaların endeksleri vardır (bunlar sadece açıklama amaçlıdır ve tabloyla birlikte açık bir şekilde saklanmaz!).
# Logical model of Python Hash table
-+-----------------+
0| <hash|key|value>|
-+-----------------+
1| ... |
-+-----------------+
.| ... |
-+-----------------+
i| ... |
-+-----------------+
.| ... |
-+-----------------+
n| ... |
-+-----------------+
Yeni bir diksiyon başlatıldığında, 8 yuva ile başlar . (bkz. dictobject.h: 49 )
i
, anahtarın karmasını temel alan bir yuva ile başlıyoruz . CPython başlangıçta kullanır i = hash(key) & mask
(nerede mask = PyDictMINSIZE - 1
, ama bu gerçekten önemli değil). Sadece i
kontrol edilen ilk yuvanın anahtarın karmasına bağlı olduğunu unutmayın .<hash|key|value>
). Peki ya bu yuva işgal edilirse !? Büyük olasılıkla başka bir girişin aynı karma (karma çarpışma!)==
karşılaştırıldığında değildir is
(sokulacak karma ve mevcut giriş anahtarı karşı yuvaya giriş karşılaştırılması) dictobject.c : 337,344-345 ). Her ikisi de eşleşirse, girişin zaten var olduğunu düşünür, vazgeçer ve eklenecek bir sonraki girişe geçer. Karma veya anahtar eşleşmezse, problamaya başlar .i+1, i+2, ...
ve ilk mevcut olanı kullanabiliriz (bu doğrusal problama). Ancak yorumlarda güzel açıklanan nedenlerden dolayı (bkz. Dictobject.c: 33-126 ), CPython rastgele problama kullanır . Rastgele problamada, bir sonraki yuva sahte rasgele bir sırada seçilir. Giriş ilk boş yuvaya eklenir. Bu tartışma için, bir sonraki yuvayı seçmek için kullanılan gerçek algoritma gerçekten önemli değildir ( problama algoritması için bkz. Dictobject.c: 33-126 ). Önemli olan, ilk boş yuva bulunana kadar yuvaların problanmasıdır.dict
üçte iki dolu ise yeniden boyutlandırılacaktır. Bu, aramaların yavaşlamasını önler. (bkz. dictobject.h: 64-65 )NOT: Bir dict içindeki birden çok girişin nasıl aynı karma değerlere sahip olabileceğiyle ilgili kendi soruma yanıt olarak Python Dict uygulaması üzerine araştırma yaptım . Burada yanıtın hafifçe düzenlenmiş bir versiyonunu yayınladım çünkü tüm araştırmalar bu soru için de çok alakalı.
Python'un Yerleşik Sözlükleri Nasıl Uygulanır?
İşte kısa kurs:
Sıralı konu, Python 3.6'dan (diğer uygulamalara devam etme şansı vermek için) gayri resmi değil, ancak Python 3.7'de resmi .
Uzun bir süre, aynen böyle çalıştı. Python 8 boş satırı önceden konumlandıracak ve anahtar / değer çiftini nereye yapıştıracağını belirlemek için karmayı kullanacaktı. Örneğin, anahtarın karması 001'de sona erdiğinde, 1 (yani 2.) dizinine yapışır (aşağıdaki örnekte olduğu gibi).
<hash> <key> <value>
null null null
...010001 ffeb678c 633241c4 # addresses of the keys and values
null null null
... ... ...
Her satır 64 bit mimaride 24 bayt, 32 bit 32 bayt alır. (Sütun başlıklarının yalnızca buradaki amaçlarımız için etiketler olduğunu unutmayın - bunlar aslında bellekte mevcut değildir.)
Karma, önceden var olan bir anahtarın hash'iyle aynı sona ererse, bu bir çarpışmadır ve daha sonra anahtar / değer çiftini farklı bir yere yapıştıracaktır.
5 anahtar / değer çifti saklandıktan sonra, başka bir anahtar / değer çifti eklenirken, karma çarpışma olasılığı çok büyüktür, bu nedenle sözlük boyutu iki katına çıkar. 64 bitlik bir işlemde, yeniden boyutlandırmadan önce 72 bayt boş ve 10 boş satır nedeniyle 240 bayt israf ediyoruz.
Bu çok yer kaplıyor, ancak arama süresi oldukça sabit. Anahtar karşılaştırma algoritması karmayı hesaplamak, beklenen konuma gitmek, anahtarın kimliğini karşılaştırmaktır - aynı nesne ise, eşittirler. Onlar eğer o zaman, karma değerlerini karşılaştırmak Değilse değil aynı, eşit değiller. Aksi halde, nihayet eşitlik için anahtarları karşılaştırırız ve eğer eşitlerse, değeri döndürürler. Eşitlik için son karşılaştırma oldukça yavaş olabilir, ancak önceki kontroller genellikle son karşılaştırmayı kısaltarak aramaları çok hızlı hale getirir.
Çarpışmalar yavaşlar ve bir saldırgan teorik olarak bir hizmet reddi saldırısı gerçekleştirmek için karma çarpışmaları kullanabilir, bu nedenle her yeni Python işlemi için farklı karmaları hesaplayacak şekilde karma işlevinin başlatılmasını rasgele seçtik.
Yukarıda açıklanan boşa harcanan alan, sözlüklerin uygulanmasını değiştirmemize yol açmıştır.
Bunun yerine, eklemenin dizini için bir diziyi önceden konumlandırarak başlarız.
İlk anahtar / değer çiftimiz ikinci yuvaya girdiğinden, aşağıdaki gibi dizine ekleriz:
[null, 0, null, null, null, null, null, null]
Ve masamız ekleme siparişiyle doldurulur:
<hash> <key> <value>
...010001 ffeb678c 633241c4
... ... ...
Bir anahtar için arama yaptığımızda, beklediğimiz konumu kontrol etmek için hash'ı kullanırız (bu durumda, doğrudan dizinin 1. dizinine gideriz), sonra hash tablosundaki bu dizine gideriz (örn. İndex 0) ), tuşların eşit olup olmadığını kontrol edin (daha önce açıklanan algoritmanın aynısını kullanarak) ve varsa değeri döndürün.
Önceden var olan uygulama üzerinde oldukça fazla yer tasarrufu sağladığımız ve ekleme talimatını koruduğumuz artışlarla birlikte, bazı durumlarda küçük hız kayıpları ve diğerlerinde kazançlar ile sürekli arama süresini koruyoruz. Boşa harcanan tek alan, dizin dizisindeki boş baytlardır.
Raymond Hettinger bunu 2012 yılının Aralık ayında python- dev'de tanıttı . Sonunda Python 3.6'da CPython'a girdi . Ekleme yoluyla sipariş vermek, Python'un diğer uygulamalarını yakalama şansı vermek için 3.6 için bir uygulama detayı olarak kabul edildi.
Yerden tasarruf etmek için bir başka optimizasyon, anahtarları paylaşan bir uygulamadır. Böylece, tüm bu alanı kaplayan fazladan sözlüklere sahip olmak yerine, paylaşılan anahtarları ve anahtarların karmalarını yeniden kullanan sözlüklerimiz var. Bunu şöyle düşünebilirsiniz:
hash key dict_0 dict_1 dict_2...
...010001 ffeb678c 633241c4 fffad420 ...
... ... ... ... ...
64 bitlik bir makine için, bu ekstra sözlük başına anahtar başına 16 bayta kadar tasarruf sağlayabilir.
Bu paylaşılan anahtar diktelerinin özel nesneler için kullanılması amaçlanmıştır __dict__
. Bu davranışı elde etmek için __dict__
, bir sonraki nesnenizi somutlaştırmadan önce nüfusunuzu doldurmanız gerektiğini düşünüyorum ( bkz. PEP 412 ). Bu, tüm özelliklerinizi __init__
veya öğesinde atamanız gerektiği anlamına gelir __new__
;
Ancak, __init__
yürüttüğünüz sırada tüm özelliklerinizi biliyorsanız __slots__
, nesnenizi de sağlayabilir ve __dict__
hiç oluşturulmadığını (ebeveynlerde mevcut değilse) __dict__
garanti edebilir, hatta öngörülen özelliklerinizin yine de yuvalarda saklanır. Hakkında daha fazlası için __slots__
, burada benim cevaba bakınız .
**kwargs
Bir fonksiyondaki sırasını koruma .find_empty_slot
: github.com/python/cpython/blob/master/Objects/dictobject.c # L969 - ve 134. satırdan başlayarak onu anlatan bazı nesirler var.
Python Sözlükler Açık adresleme kullanır ( Güzel kod içinde referans )
NB! Açık adresleme , aka kapalı karma , Wikipedia'da belirtildiği gibi, ters açık karma ile karıştırılmamalıdır!
Açık adresleme, diktenin dizi yuvalarını kullandığı anlamına gelir ve bir nesnenin dikte içinde birincil konumu alındığında, nesnenin karma değeri, nesnenin karma değerinin oynadığı "perturbation" şeması kullanılarak aynı dizideki farklı bir dizinde aranır. .