Python'un Yerleşik Sözlükleri Nasıl Uygulanır?


294

Python için yerleşik sözlük türünün nasıl uygulandığını bilen var mı? Anladığım kadarıyla bu bir çeşit karma tablo, ama kesin bir cevap bulamadım.


4
İşte 2.7'den 3.6'ya kadar Python sözlükleri hakkında anlayışlı bir konuşma. Link
Sören

Yanıtlar:


494

İşte Python'un bir araya getirebildiğim dikte ettiği her şey (muhtemelen herkesin bilmek istediklerinden daha fazla; ancak cevap kapsamlı).

  • Python sözlükleri karma tablolar olarak uygulanır .
  • Karma tablolar karma çarpışmalara izin vermelidir, yani iki farklı anahtar aynı karma değerine sahip olsa bile, tablonun uygulamasında anahtar ve değer çiftlerini açık bir şekilde ekleme ve alma stratejisi olmalıdır.
  • Python dict, karma çarpışmaları çözmek için açık adresleme kullanır (aşağıda açıklanmıştır) (bkz. Dictobject.c: 296-297 ).
  • Python hash tablosu sadece bitişik bir bellek bloğudur (bir dizi gibi, böylece O(1)indekse göre arama yapabilirsiniz ).
  • Tablodaki her yuva bir ve yalnızca bir girişi saklayabilir. Bu önemli.
  • Tablodaki her giriş aslında üç değerin birleşimidir: <hash, key, value> . Bu bir C yapısı olarak uygulanır (bkz. Dictobject.h: 51-56 ).
  • 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 )

  • Tabloya giriş eklerken 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 ikontrol edilen ilk yuvanın anahtarın karmasına bağlı olduğunu unutmayın .
  • Bu yuva boşsa, giriş yuvaya eklenir (girişle, yani <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!)
  • Diliminin meşgul ise, CPython (ve hatta PyPy) karşılaştırır karma ve anahtar (karşılaştırma I ortalama ile ==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 .
  • Problama, boş bir yuva bulmak için yuvaları yuvaya göre aradığı anlamına gelir. Teknik olarak sadece birer birer gidebilir 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.
  • Aynı şey aramalar için de geçerlidir, sadece ilk yuva i ile başlar (burada i anahtarın hash'ına bağlıdır). Karma ve anahtar yuvadaki girişle eşleşmezse, eşleşen bir yuva bulana kadar problamaya başlar. Tüm yuvalar bittiyse, arıza olduğunu bildirir.
  • BTW, 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ı.


9
Hem hash hem de anahtar eşleştiğinde, (op ekle) vazgeçer ve devam eder. Ekleme bu durumda mevcut girişin üzerine yazmıyor mu?
0xc0de

65

Python'un Yerleşik Sözlükleri Nasıl Uygulanır?

İşte kısa kurs:

  • Bunlar karma tablolardır. (Python uygulamasının ayrıntıları için aşağıya bakın.)
  • Python 3.6'dan itibaren yeni bir düzen ve algoritma onları
    • anahtar yerleştirme ile sıralanır ve
    • daha az yer kaplar,
    • performansta neredeyse hiç maliyet olmadan.
  • Başka bir optimizasyon, anahtarları paylaştığında yer tasarrufu sağlar (özel durumlarda).

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 .

Python'un Sözlükleri Karma Tablolardır

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.

Yeni Kompakt Karma Tablolar

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.

Paylaşılan Anahtarlar

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.

Özel Nesneler ve Alternatifler için Paylaşılan Anahtarlar

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 .

Ayrıca bakınız:


1
"Biz" ve "Python'un diğer uygulamalarına yetişme şansı vermek için" dediniz - bu, "bir şeyler bildiğiniz" anlamına mı geliyor ve kalıcı bir özellik olabilir mi? Spec tarafından sipariş edilen diktelerin herhangi bir dezavantajı var mı?
toonarmycaptain

Siparişin dezavantajı, eğer emirlerin sipariş edilmesi bekleniyorsa, kolayca sipariş edilmeyen daha iyi / daha hızlı bir uygulamaya geçemezler. Olsa da böyle olması muhtemel görünmüyor. Ben "bir şeyler biliyorum" çünkü çekirdek üyeler ve benden daha iyi bir gerçek dünya itibarı olan başkaları tarafından yazılmış çok fazla konuşma izliyorum ve okuduğum bir kaynak olmasa bile, genellikle biliyorum neden bahsettiğimi. Ama bence bu noktayı Raymond Hettinger'in konuşmalarından birinden alabilirsiniz.
Aaron Hall

1
Eklemenin nasıl çalıştığını biraz belirsiz bir şekilde açıkladınız ("Karma önceden var olan bir anahtarın hashiyle aynı şekilde sona ererse, ... o zaman anahtar / değer çiftini farklı bir yere yapıştıracaktır" - herhangi bir?) arama ve üyelik testi nasıl çalışır. Konumu ya karma tarafından nasıl belirlenir oldukça açık değil, ama boyutu her zaman 2 bir güç olduğunu varsayalım ve karma son birkaç bit almak ...
Alexey

@Alexey Sağladığım son bağlantı, şu anda 969 satırında şu işlevi gören bulabileceğiniz iyi açıklamalı diksiyon uygulamasını sağlar find_empty_slot: github.com/python/cpython/blob/master/Objects/dictobject.c # L969 - ve 134. satırdan başlayarak onu anlatan bazı nesirler var.
Aaron Hall

46

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. .


5
diyerek şöyle devam etti: "karşıt açık karma ile karıştırılmamalıdır! (kabul edilen cevapta gördüğümüz)." - Bunu yazdığınızda hangi cevabın kabul edildiğinden veya o cevabın o anda ne söylendiğinden emin değilim - ancak bu parantezli yorum şu anda kabul edilen cevap için geçerli değil ve en iyisi kaldırılacak.
Tony Delroy
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.