Sözlüklerdeki ve setlerdeki düzen neden keyfi?


151

Bir sözlükte döngü yapmanın veya python'da ayarlamanın 'keyfi' sıra ile nasıl yapıldığını anlamıyorum.

Demek istediğim, bu bir programlama dilidir, bu yüzden dildeki her şey% 100 belirlenmelidir, doğru mu? Python, sözlüğün veya kümenin hangi kısmının seçildiğine karar veren bir çeşit algoritmaya sahip olmalıdır, 1., saniye vb.

Neyi kaçırıyorum?


1
En yeni PyPy derlemesi (2.5, Python 2.7 için) varsayılan olarak sipariş edilen sözlükleri yapar .
Veedrac

Yanıtlar:


236

Not: Bu cevap, dictPython 3.6'da türün uygulanmasından önce yazılmıştır . Bu yanıttaki uygulama ayrıntılarının çoğu hala geçerlidir, ancak sözlüklerdeki anahtarların listeleme sırası artık karma değerleri ile belirlenmemektedir. Ayarlanan uygulama değişmeden kalır.

Sipariş keyfi değildir, ancak sözlüğün veya kümenin ekleme ve silme geçmişine ve ayrıca belirli Python uygulamasına bağlıdır. Bu cevabın geri kalanında, 'sözlük' için, 'küme' de okuyabilirsiniz; kümeler sadece anahtarları olan ve değerleri olmayan sözlükler olarak uygulanır.

Anahtarlar karmadır ve karma değerleri dinamik bir tablodaki yuvalara atanır (ihtiyaca göre büyüyebilir veya daralabilir). Ve bu haritalama işlemi çarpışmalara yol açabilir, yani bir anahtarın zaten orada olana dayanarak bir sonraki yuvaya yerleştirilmesi gerekecektir .

İçerik döngülerini yuvaların üzerinde listelemek, böylece anahtarlar o anda tabloda bulundukları sırayla listelenir .

Anahtarları al 'foo've 'bar', örneğin, ve masa boyutu 8 yuvaları olduğunu varsayalım. Python 2.7 yılında hash('foo')ise -4177197833195190597, hash('bar')olduğu 327024216814240868. Modulo 8, bu iki anahtarın 3 ve 4 numaralı yuvalara yerleştirildiği anlamına gelir:

>>> hash('foo')
-4177197833195190597
>>> hash('foo') % 8
3
>>> hash('bar')
327024216814240868
>>> hash('bar') % 8
4

Bu, listeleme sırasını bildirir:

>>> {'bar': None, 'foo': None}
{'foo': None, 'bar': None}

3 ve 4 dışındaki tüm yuvalar boştur, tablonun üzerinde döngü ilk önce yuva 3'ü, ardından yuva 4'ü listeler, böylece 'foo'daha önce listelenir 'bar'.

barve baz, bununla birlikte, aynı yarık eşlemek ayrı ve böylece tam olarak 8 olan karma değerlerine sahiptir 4:

>>> hash('bar')
327024216814240868
>>> hash('baz')
327024216814240876
>>> hash('bar') % 8
4
>>> hash('baz') % 8
4

Onların sırası şimdi ilk önce hangi anahtarın yerleştirildiğine bağlıdır; ikinci anahtarın bir sonraki yuvaya taşınması gerekir:

>>> {'baz': None, 'bar': None}
{'bar': None, 'baz': None}
>>> {'bar': None, 'baz': None}
{'baz': None, 'bar': None}

Tablo sırası burada farklılık gösterir, çünkü önce bir veya diğer anahtar yuvalanmıştır.

CPython (en yaygın olarak kullanılan Python uygulaması) tarafından kullanılan temel yapının teknik adı , açık adresleme kullanan bir karma tablodur . Merak ediyorsanız ve C'yi yeterince iyi anlarsanız, tüm (iyi belgelenmiş) ayrıntılar için C uygulamasına bakın. CPython'un nasıl dictçalıştığı hakkında Brandon Rhodes tarafından hazırlanan bu Pycon 2010 sunumunu izleyebilir veya Güzel Kod'un bir kopyasını alabilirsiniz Andrew Kuchling tarafından yazılmış uygulama hakkında bir bölüm içeren alabilirsiniz.

Python 3.3'ten itibaren rastgele bir karma tohumun da kullanıldığını ve belirli hizmet reddi türlerini önlemek için karma çarpışmaları öngörülemez hale getirdiğini (bir saldırganın bir Python sunucusunu toplu karma çarpışmalara neden olarak yanıt vermediği durumlarda) unutmayın. Bu, belirli bir sözlüğün veya kümenin sırasının geçerli Python çağrısı için rastgele karma tohumuna da bağlı olduğu anlamına gelir .

Diğer uygulamalar, bunlar için belgelenmiş Python arayüzünü karşıladıkları sürece sözlükler için farklı bir yapı kullanmakta özgürdürler, ancak şimdiye kadar tüm uygulamaların hash tablosunun bir varyasyonunu kullandığına inanıyorum.

CPython 3.6 , ekleme sırasını koruyan ve önyükleme için daha hızlı ve bellekte daha verimli olan yeni bir dict uygulama sunar. Her satırda saklanan karma değerine ve anahtar ve değer nesnelerine başvurduğu büyük bir seyrek tablo tutmak yerine, yeni uygulama , yalnızca ayrı bir 'yoğun' tablodaki (yalnızca çok sayıda satır içeren bir dizin) indekslere başvuran daha küçük bir karma dizisi ekler gerçek anahtar / değer çiftleri olduğu için) ve içerilen öğeleri sırayla listeleyen yoğun tablodur. Daha fazla ayrıntı için Python-Dev önerisine bakın . Python 3.6'da bunun bir uygulama detayı olarak kabul edildiğini unutmayın, Python-the-language diğer uygulamaların düzeni koruması gerektiğini belirtmez. Bu durum, bu detayın bir dil spesifikasyonu olarak yükseltildiği Python 3.7'de değişti ; herhangi bir uygulamanın Python 3.7 veya daha yeni bir sürümüyle düzgün bir şekilde uyumlu olması için , bu sipariş koruma davranışını kopyalaması gerekir . Açık olmak gerekirse: bu değişiklik kümeler için geçerli değildir, çünkü kümeler zaten 'küçük' bir karma yapısına sahiptir.

Python 2.7 ve daha yeni sürümleri, bir OrderedDictsınıf , dictanahtar sırasını kaydetmek için ek bir veri yapısı ekleyen bir alt sınıf sağlar . Biraz hız ve ekstra bellek fiyatına sahip olan bu sınıf, anahtarları hangi sırayla eklediğinizi hatırlar; anahtarları, değerleri veya öğeleri listelemek bu sırayla bunu yapar. Siparişi verimli bir şekilde güncel tutmak için ek bir sözlükte saklanan çift bağlantılı bir liste kullanır. Fikri ana hatlarıyla veren Raymond Hettinger'in gönderisine bakın . OrderedDictnesnelerin yeniden düzenlenebilir olma gibi başka avantajları da vardır .

Sipariş edilen bir set istiyorsanız, osetpaketi yükleyebilirsiniz ; Python 2.5 ve sonraki sürümlerde çalışır.


1
Diğer Python uygulamalarının karma tablo olmayan bir şeyi şu ya da bu şekilde kullanabileceğini sanmıyorum (şimdi karma tabloları uygulamak için milyarlarca farklı yol olmasına rağmen, hala biraz özgürlük var). Sözlüklerin kullandığı __hash__ve __eq__(ve başka hiçbir şeyin olmadığı) gerçekte bir uygulama garantisi değil, bir dil garantisidir.

1
@delnan: Yine de karma ve eşitlik testleriyle bir BTree kullanıp kullanamayacağınızı merak ediyorum. :-)
Martijn Pieters

1
Kesinlikle doğru ve yanlış wrt fizibilitesi kanıtlanmış olmaktan mutluluk duyarım, ama daha geniş bir sözleşme gerektirmeden bir karma tabloyu yenebilir hiçbir şekilde görmüyorum. BTree daha iyi bir ortalama-durum performansına sahip olmaz ve size daha kötü-durum da vermez (karma çarpışmaları hala doğrusal arama anlamına gelir). Böylece, sadece birçok karma neomg uyumlu (mod tablo boyutu) için daha iyi direnç kazanırsınız ve bunu (bazıları kullanılır dictobject.c) ele almanın ve BTree'nin doğru bulması için çok daha az karşılaştırma yapmanın birçok harika yolu vardır. alt ağaç.

@delnan: Tamamen katılıyorum; En önemlisi, diğer uygulama seçeneklerine izin vermediği için basmak istemedim.
Martijn Pieters

37

Bu, Python 3.41 A setinin kopya olarak kapatılmasından önce verilen bir yanıttır .


Diğerleri haklı: siparişe güvenmeyin. Bir tane varmış gibi yapma.

Bununla birlikte, güvenebileceğiniz bir şey var:

list(myset) == list(myset)

Yani, düzen kararlıdır .


Neden algılanan bir düzen olduğunu anlamak için birkaç şeyin anlaşılması gerekir:

  • Python'un karma kümeleri kullandığını ,

  • CPython'un karma kümesi bellekte nasıl saklanır ve

  • Sayılar nasıl özetlenir?

Üstten:

Bir karma seti gerçekten hızlı arama süreleri ile rastgele veri saklama yöntemidir.

Bir destek dizisi vardır:

# A C array; items may be NULL,
# a pointer to an object, or a
# special dummy object
_ _ 4 _ _ 2 _ _ 6

Bu setlerden çıkarmayacağımız için, yalnızca kaldırmaları daha kolay halletmek için var olan özel kukla nesneyi görmezden geleceğiz.

Gerçekten hızlı bir arama yapmak için, bir nesneden bir karma hesaplamak için biraz sihir yaparsınız. Tek kural, eşit olan iki nesnenin aynı karmaya sahip olmasıdır. (Ancak iki nesne aynı karmaya sahipse, eşit olmayabilir.)

Ardından, modülü dizi uzunluğuna göre alarak dizini oluşturursunuz:

hash(4) % len(storage) = index 2

Bu, öğelere erişmeyi gerçekten hızlı hale getirir.

Hash'ler olarak, hikayenin sadece çoğu hash(n) % len(storage)ve hash(m) % len(storage)aynı sayıda neden olabilir. Bu durumda, birkaç farklı strateji çatışmayı çözmeyi deneyebilir. CPython, karmaşık şeyler yapmadan önce 9 kez "lineer problama" kullanır, bu nedenle başka bir yere bakmadan önce 9 yere kadar yuvanın soluna bakacaktır .

CPython'un karma setleri şu şekilde saklanır:

  • Bir karma seti en fazla 2/3 dolu olabilir . 20 öğe varsa ve destek dizisi 30 öğe uzunluğunda ise, destek deposu daha büyük olacak şekilde yeniden boyutlandırılır. Bunun nedeni, küçük destek mağazalarıyla çarpışmaları daha sık görmeniz ve çarpışmaların her şeyi yavaşlatmasıdır.

  • Destek deposu, ikisinin gücü ile yeniden boyutlandırılan büyük setler (50k elemanlar) hariç, 8'den başlayarak 4'lük güçlerde yeniden boyutlandırılır: (8, 32, 128, ...).

Yani bir dizi oluşturduğunuzda, destek deposu uzunluk 8'dir. 5 dolu olduğunda ve bir öğe eklediğinizde, kısaca 6 öğe içerecektir. 6 > ²⁄₃·8böylece bu yeniden boyutlandırmayı tetikler ve destek deposu 32 boyuta dört katına çıkar.

Son olarak, hash(n)sadece nsayılar için döner ( -1özel olanlar hariç ).


Öyleyse, ilkine bakalım:

v_set = {88,11,1,33,21,3,7,55,37,8}

len(v_set)10'dur, bu nedenle tüm mağazalar eklendikten sonra destek mağazası en az 15 (+1) olur . 2'nin ilgili gücü 32'dir. Yani destek mağazası:

__ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __

Sahibiz

hash(88) % 32 = 24
hash(11) % 32 = 11
hash(1)  % 32 = 1
hash(33) % 32 = 1
hash(21) % 32 = 21
hash(3)  % 32 = 3
hash(7)  % 32 = 7
hash(55) % 32 = 23
hash(37) % 32 = 5
hash(8)  % 32 = 8

böylece bunlar şu şekilde eklenir:

__  1 __  3 __ 37 __  7  8 __ __ 11 __ __ __ __ __ __ __ __ __ 21 __ 55 88 __ __ __ __ __ __ __
   33 ← Can't also be where 1 is;
        either 1 or 33 has to move

Yani biz gibi bir sipariş beklenir

{[1 or 33], 3, 37, 7, 8, 11, 21, 55, 88}

başka bir yerde başlangıçta olmayan 1 veya 33 ile. Bu doğrusal problama kullanır, bu yüzden ya:

       ↓
__  1 33  3 __ 37 __  7  8 __ __ 11 __ __ __ __ __ __ __ __ __ 21 __ 55 88 __ __ __ __ __ __ __

veya

       ↓
__ 33  1  3 __ 37 __  7  8 __ __ 11 __ __ __ __ __ __ __ __ __ 21 __ 55 88 __ __ __ __ __ __ __

33'ün yerinden edilmiş olanı olmasını bekleyebilirsiniz, çünkü 1 zaten oradaydı, ancak set inşa edilirken gerçekleşen yeniden boyutlandırma nedeniyle, aslında durum böyle değil. Set her yeniden oluşturulduğunda, daha önce eklenmiş olan eşyalar etkili bir şekilde yeniden sıralanır.

Şimdi nedenini görebilirsiniz

{7,5,11,1,4,13,55,12,2,3,6,20,9,10}

olabilir. 14 element var, bu yüzden destek mağazası en az 21 + 1, yani 32:

__ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __

İlk 13 yuvada 1 ila 13 karma. 20, yuva 20'ye gider.

__  1  2  3  4  5  6  7  8  9 10 11 12 13 __ __ __ __ __ __ 20 __ __ __ __ __ __ __ __ __ __ __

55 hash(55) % 32, 23 olan yuvaya gider :

__  1  2  3  4  5  6  7  8  9 10 11 12 13 __ __ __ __ __ __ 20 __ __ 55 __ __ __ __ __ __ __ __

Bunun yerine 50 tane seçseydik,

__  1  2  3  4  5  6  7  8  9 10 11 12 13 __ __ __ __ 50 __ 20 __ __ __ __ __ __ __ __ __ __ __

Ve bakalım:

{1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13, 20, 50}
#>>> {1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13, 50, 20}

pop şeylerin görünüşüyle ​​oldukça basit bir şekilde uygulanır: listeyi dolaşır ve birincisini açar.


Bunların hepsi uygulama detayı.


17

"Keyfi", "belirlenmemiş" ile aynı şey değildir.

Söyledikleri, "ortak arayüzde" olan sözlük yineleme sırasının yararlı özellikleri olmadığıdır. Hemen hemen sözlük yinelemesini uygulayan kod tarafından belirlenen yineleme sırasının neredeyse kesinlikle birçok özelliği vardır, ancak yazarlar bunları kullanabileceğiniz bir şey olarak size vaat etmemektedir. Bu, programınızın kırılacağından endişe etmeden bu özellikleri Python sürümleri arasında (veya yalnızca farklı çalışma koşullarında veya çalışma zamanında tamamen rastgele) değiştirme konusunda daha fazla özgürlük sağlar.

Bu nedenle , sözlük sırasının herhangi bir özelliğine bağlı bir program yazarsanız , sözlük türünü kullanmanın "sözleşmesini bozarsınız" ve Python geliştiricileri, işe yaramış gibi görünse bile bunun her zaman işe yarayacağına söz vermezler. Şimdilik test ettiğinizde. Temelde C'deki "tanımlanmamış davranış" a güvenmekle eşdeğerdir.


3
Sözlük yinelemesinin bir bölümünün iyi tanımlandığını unutmayın: Belirli bir sözlüğün anahtarları, değerleri veya öğeleri üzerinde yineleme yapmak, sözlükte hiçbir değişiklik yapılmadığı sürece aynı sırada gerçekleşir. Bu d.items(), esasen aynı olduğu anlamına gelir zip(d.keys(), d.values()). Ancak sözlüğe herhangi bir öğe eklenirse, tüm bahisler kapalıdır. Sipariş tamamen değişebilir (karma tablosunun yeniden boyutlandırılması gerekiyorsa), ancak çoğu zaman yeni öğeyi dizide rastgele bir noktada bulursunuz.
Blckknght

6

Bu sorunun diğer cevapları mükemmel ve iyi yazılmış. OP, "nasıl" diye "benim" nasıl kaçtıkları "veya" neden "olarak yorumladığımı soruyor.

Python belgelerine diyor sözlükler Python sözlük aletlerin çünkü sipariş edilmez soyut veri tipi ilişkisel dizi . Söyledikleri gibi

ciltlerin iade edilme sırası keyfi olabilir

Başka bir deyişle, bir bilgisayar bilimi öğrencisi ilişkilendirilebilir bir dizinin sipariş edildiğini varsayamaz. Aynı şey matematikteki setler için de geçerlidir

bir kümenin elemanlarının listelendiği sıra önemsizdir

ve bilgisayar bilimi

bir küme, belirli bir sipariş vermeden belirli değerleri depolayabilen soyut bir veri türüdür

Bir karma tablo kullanarak sözlük uygulamak , sıra ile ilgili olarak ilişkilendirilebilir dizilerle aynı özelliklere sahip olması bakımından ilginç olan bir uygulama ayrıntısıdır .


1
Sen temelde doğru ama biraz daha yakın olmak (ve "sırasız" var sebeple iyi bir ipucu vermek) bir bir uygulamasıdır söylemek istiyorum karma tablo ziyade bir doç dizisi.
İki Bit Simyacı

5

Python kullanım karma tablosu sözlükleri saklamak için , bu nedenle sözlüklerde veya karma tablosu kullanan diğer yinelenebilir nesnelerde düzen yoktur.

Ama bir karma nesne öğelerin endekslerini ilişkin, piton kodunu aşağıdaki dayalı endeksleri hesaplamak içindehashtable.c :

key_hash = ht->hash_func(key);
index = key_hash & (ht->num_buckets - 1);

Bu nedenle, tamsayıların karma değeri tamsayının kendisi olduğu için * indeks sayıya ( ht->num_buckets - 1sabittir) dayalıdır, bu nedenle endeks Bitwise- ve arasında (ht->num_buckets - 1)ve sayının kendisi tarafından hesaplanır * (karma değerini -1 olan -1 için bekliyoruz ) ve karma değeri olan diğer nesneler için.

sethash tablosunu kullanan aşağıdaki örneği göz önünde bulundurun :

>>> set([0,1919,2000,3,45,33,333,5])
set([0, 33, 3, 5, 45, 333, 2000, 1919])

Sayı 33için var:

33 & (ht->num_buckets - 1) = 1

Aslında bu:

'0b100001' & '0b111'= '0b1' # 1 the index of 33

Not Bu durumda (ht->num_buckets - 1)olduğunu 8-1=7veya0b111 .

Ve için 1919:

'0b11101111111' & '0b111' = '0b111' # 7 the index of 1919

Ve için 333:

'0b101001101' & '0b111' = '0b101' # 5 the index of 333

Python hash işlevi hakkında daha fazla bilgi için python kaynak kodundan aşağıdaki alıntıları okumak iyi :

İlerideki ana incelikler: Çoğu karma şeması, rastgele simüle etme anlamında "iyi" bir karma fonksiyonuna sahip olmaya bağlıdır. Python şunları yapmaz: en önemli karma işlevleri (dizeler ve ints için) yaygın durumlarda çok düzenli:

>>> map(hash, (0, 1, 2, 3))
  [0, 1, 2, 3]
>>> map(hash, ("namea", "nameb", "namec", "named"))
  [-1658398457, -1658398460, -1658398459, -1658398462]

Bu mutlaka kötü değil! Aksine, 2 ** i boyutundaki bir tabloda, düşük sıralı i bitlerini ilk tablo dizini olarak almak son derece hızlıdır ve bitişik bir dizi aralık tarafından endekslenen dikmeler için hiç çarpışma yoktur. Aynı durum, tuşlar "ardışık" dizeler olduğunda yaklaşık olarak doğrudur. Bu, yaygın durumlarda rastgele olmayandan daha iyi davranış sağlar ve bu çok arzu edilir.

OTOH, çarpışmalar meydana geldiğinde, karma tablonun bitişik dilimlerini doldurma eğilimi, iyi bir çarpışma çözümleme stratejisini önemli hale getirir. Karma kodun yalnızca son i bitlerini almak da savunmasızdır: örneğin, listeyi [i << 16 for i in range(20000)]bir anahtar kümesi olarak düşünün . Ints kendi hash kodları olduğundan ve bu 2 ** 15 boyutunda bir dikte uyduğundan, her hash kodunun son 15 bitinin tümü 0'dır: hepsi aynı tablo indeksine haritası.

Ancak olağandışı vakalara yemek yapmak olağan olanları yavaşlatmamalı, bu yüzden sadece son i bitlerini yine de alıyoruz. Gerisini yapmak çarpışma çözümüne bağlı. Biz ise genellikle biz ilk denemede aradığınız anahtarı bulmaya (ve, o çıkıyor, genellikle yaptığımız - oran lehimize sağlam böylece masa yük faktörü, 2/3 altında tutulur), o zaman başlangıç ​​endeksi hesaplama kir ucuz tutmak için en mantıklı.


* Sınıf için hash işlevi int:

class int:
    def __hash__(self):
        value = self
        if value == -1:
            value = -2
        return value


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.