Bir sözlük karma mı?


156

Önbellekleme amaçları için, bir sözlükte bulunan GET argümanlarından bir önbellek anahtarı oluşturmam gerekiyor.

Şu anda kullanıyorum sha1(repr(sorted(my_dict.items())))( hashlib'i dahili olarak sha1()kullanan bir kolaylık yöntemidir ) ama daha iyi bir yol olup olmadığını merak ediyorum.


4
bu iç içe dikte ile çalışmayabilir. En kısa çözüm, bunun yerine dikte değerlerine geri dönecek olan json.dumps (my_dict, sort_keys = True) kullanmaktır.
Andrey Fedorov

2
FYI re: dumps, stackoverflow.com/a/12739361/1082367 "Turşudan elde edilen çıktının, siparişi belirleyici olmayacak şekilde dikte etmek ve ayarlamak için benzer nedenlerle kanonik olduğu garanti edilmez. Karma için turşu veya pprint veya repr kullanmayın ."
Matthew Cornell

dict tuşlarını sıralamak öğeleri değil, aynı zamanda hash fonksiyonuna tuşları gönderirim.
nyuwec

2
Değişken veri yapılarının (sözlükler gibi) hash edilmesiyle ilgili ilginç bir arka plan: python.org/dev/peps/pep-0351 , nesnelerin keyfi olarak dondurulmasına izin vermek için önerildi, ancak reddedildi. Gerekçe için, python- dev'deki
FluxLemur

Verileriniz json biçimindeyse ve anlamsal olarak değişmez karma istiyorsanız, github.com/schollii/sandals/blob/master/json_sem_hash.py adresine bakın . İç içe geçmiş yapılarda (elbette, json'dan beri) çalışır ve korunmuş düzen (python'un ömrü boyunca evrimleşmiş) gibi diktenin içlerine bağlı değildir ve iki veri yapısı anlamsal olarak aynı ise aynı karma değerini verecektir ( gibi {'a': 1, 'b':2}anlam ile aynıdır {'b':2, 'a':1}). Henüz çok karmaşık bir şey üzerinde kullanmadım, bu yüzden YMMV ama geri bildirim hoş geldiniz.
Oliver

Yanıtlar:


110

Sözlüğünüz iç içe değilse, diktenin öğeleriyle bir frozenset yapabilir ve kullanabilirsiniz hash():

hash(frozenset(my_dict.items()))

Bu, JSON dizesini veya sözlüğün temsilini oluşturmaktan çok daha az hesaplama gerektirir.

GÜNCELLEME: Bu yaklaşımın neden istikrarlı bir sonuç vermeyebileceğini lütfen aşağıdaki yorumlara bakın.


9
Bu benim için iç içe bir sözlükle işe yaramadı. Aşağıdaki çözümü denemedim (çok karmaşık). OP'nin çözümü mükemmel çalışıyor. Bir ithalat kaydetmek için sha1'i karma ile değiştirdim.
Ocak'ta spatel

9
@Ceaser Bu çalışmaz çünkü demet sipariş vermeyi gerektirir, ancak dikte öğeleri sırasızdır. frozenset daha iyidir.
Antimon

28
Farklı makineler arasında bir şeyin tutarlı olması gerekiyorsa, yerleşik karma değerine dikkat edin. Heroku ve GAE gibi bulut platformlarında python uygulamaları, iki veya daha fazla "makine" (heroku durumunda dinamolar) arasında paylaşılması gereken herhangi bir şey için işe yaramaz hale getiren farklı durumlarda karma () için farklı değerler döndürür
Ben Roberts

6
hash()Fonksiyonun kararlı bir çıktı üretmemesi ilginç olabilir . Bu, aynı girdi verildiğinde, aynı python yorumlayıcısının farklı örnekleriyle farklı sonuçlar döndürdüğü anlamına gelir. Bana göre, tercüman her başlatıldığında bir çeşit tohum değeri üretiliyor gibi görünüyor.
Hermann Schachner

7
beklenen. bir tür bellek rasgeleleştirmesi eklemeyi hatırladığım kadarıyla tohum güvenlik nedeniyle tanıtıldı. Dolayısıyla, iki python işlemi arasında karma
işlemenin

137

Kullanmak sorted(d.items())bize istikrarlı bir repr almak için yeterli değildir. Değerlerin bazıları dda sözlük olabilir ve anahtarları yine de keyfi bir sırayla ortaya çıkacaktır. Tüm tuşlar dize olduğu sürece kullanmayı tercih ederim:

json.dumps(d, sort_keys=True)

Bununla birlikte, karmaların farklı makineler veya Python sürümleri arasında kararlı olması gerekiyorsa, bunun kurşun geçirmez olduğundan emin değilim. Kendinizi buradaki varsayılan değişikliklerden korumak için separatorsve ensure_asciibağımsız değişkenlerini eklemek isteyebilirsiniz . Yorumları takdir ediyorum.


6
Bu sadece paranoyak olmakla birlikte, JSON çoğu karakterin herhangi bir değişmez çıkış olmadan dizelerde görünmesine izin verir, bu nedenle kodlayıcı karakterlerden kaçma veya sadece geçme konusunda bazı seçimler yapar. Bu durumda risk, kodlayıcının farklı sürümlerinin (veya gelecekteki sürümlerinin) varsayılan olarak farklı kaçış seçimleri yapabilmesidir ve programınız farklı ortamlarda aynı sözlük için farklı karma değerleri hesaplayacaktır. ensure_asciiArgüman bu tamamen varsayımsal problemine karşı koruyacaktır.
Jack O'Connor

4
Bunun performansını farklı veri kümesiyle test ettim, çok daha hızlı make_hash. gist.github.com/charlax/b8731de51d2ea86c6eb9
charlax

3
bu o kadar güvenli değil bu yüzden @charlax ujson, dict çiftleri sırasını garanti etmez
arthurprs

11
Bu çözüm yalnızca tüm anahtarlar dize olduğu sürece çalışır, örn. Json.dumps ({'a': {(0, 5): 5, 1: 3}}) başarısız olur.
Kadee

5
@LorenzoBelli, sen ekleyerek bu üstesinden gelebilir default=striçin dumpskomuta. Güzel çalışıyor gibi görünüyor.
mlissner

63

DÜZENLEME : Tüm anahtarlarınız dize ise , bu yanıtı okumaya devam etmeden önce, lütfen Jack O'Connor'ın önemli ölçüde daha basit (ve daha hızlı) çözümüne bakın (bu da iç içe sözlükler için de geçerlidir).

Her ne kadar bir cevap kabul edilmiş olsa da, sorunun başlığı "bir python sözlüğünü karıştırmak" tır ve bu başlık ile ilgili cevap eksiktir. (Sorunun içeriği ile ilgili olarak cevap tamamlandı.)

İç İçe Sözlükler

Bir sözlük nasıl karmalaştırılacağı için Stack Overflow ararsa, bu uygun başlıklı soruya rastlayabilir ve iç içe sözlükleri çoğaltmaya çalışıyorsa tatminsiz bırakılabilir. Yukarıdaki cevap bu durumda işe yaramaz ve hash'ı almak için bir çeşit özyinelemeli mekanizma uygulamanız gerekir.

İşte böyle bir mekanizma:

import copy

def make_hash(o):

  """
  Makes a hash from a dictionary, list, tuple or set to any level, that contains
  only other hashable types (including any lists, tuples, sets, and
  dictionaries).
  """

  if isinstance(o, (set, tuple, list)):

    return tuple([make_hash(e) for e in o])    

  elif not isinstance(o, dict):

    return hash(o)

  new_o = copy.deepcopy(o)
  for k, v in new_o.items():
    new_o[k] = make_hash(v)

  return hash(tuple(frozenset(sorted(new_o.items()))))

Bonus: Nesneleri ve Sınıfları Karma

hash()Eğer sınıfları veya örneklerini karma zaman fonksiyonu iyi çalışıyor. Ancak, nesnelerle ilgili olarak karma ile bulduğum bir sorun var:

class Foo(object): pass
foo = Foo()
print (hash(foo)) # 1209812346789
foo.a = 1
print (hash(foo)) # 1209812346789

Çileyi değiştirdikten sonra bile karma aynı. Çünkü foo kimliği değişmedi, bu yüzden karma aynı. Foo'nun mevcut tanımına bağlı olarak farklı şekilde karma yapmasını istiyorsanız, çözüm aslında değişen her şeyi karma etmektir. Bu durumda, __dict__özellik:

class Foo(object): pass
foo = Foo()
print (make_hash(foo.__dict__)) # 1209812346789
foo.a = 1
print (make_hash(foo.__dict__)) # -78956430974785

Ne yazık ki, sınıfın kendisiyle aynı şeyi yapmaya çalıştığınızda:

print (make_hash(Foo.__dict__)) # TypeError: unhashable type: 'dict_proxy'

Class __dict__özelliği normal bir sözlük değildir:

print (type(Foo.__dict__)) # type <'dict_proxy'>

Sınıfları uygun şekilde işleyecek öncekine benzer bir mekanizma:

import copy

DictProxyType = type(object.__dict__)

def make_hash(o):

  """
  Makes a hash from a dictionary, list, tuple or set to any level, that 
  contains only other hashable types (including any lists, tuples, sets, and
  dictionaries). In the case where other kinds of objects (like classes) need 
  to be hashed, pass in a collection of object attributes that are pertinent. 
  For example, a class can be hashed in this fashion:

    make_hash([cls.__dict__, cls.__name__])

  A function can be hashed like so:

    make_hash([fn.__dict__, fn.__code__])
  """

  if type(o) == DictProxyType:
    o2 = {}
    for k, v in o.items():
      if not k.startswith("__"):
        o2[k] = v
    o = o2  

  if isinstance(o, (set, tuple, list)):

    return tuple([make_hash(e) for e in o])    

  elif not isinstance(o, dict):

    return hash(o)

  new_o = copy.deepcopy(o)
  for k, v in new_o.items():
    new_o[k] = make_hash(v)

  return hash(tuple(frozenset(sorted(new_o.items()))))

İstediğiniz birçok öğenin karma grubunu döndürmek için bunu kullanabilirsiniz:

# -7666086133114527897
print (make_hash(func.__code__))

# (-7666086133114527897, 3527539)
print (make_hash([func.__code__, func.__dict__]))

# (-7666086133114527897, 3527539, -509551383349783210)
print (make_hash([func.__code__, func.__dict__, func.__name__]))

NOT: yukarıdaki kodların tümü Python 3.x'i varsayar. make_hash()2.7.2 'de çalışacağını varsaysam da önceki sürümlerde test yapmadım. Uzak örnekler işi yapmak kadar, ben do biliyorum

func.__code__ 

ile değiştirilmelidir

func.func_code

isinstance ikinci argüman için bir sıra alır, bu yüzden isinstance (o, (set, tuple, list)) işe yarayacaktır.
Xealot

beni frozenset sorgulama parametreleri sürekli hash olabilir farkında yapmak için teşekkürler :)
Xealot

1
Dikte edilen öğe sırası farklıysa ancak anahtar değerler farklıysa -> karma değeri (tuple (frozenset (sorted (new_o.items ()))))
Bas Koopmans

Güzel! Ayrıca hashliste ve gruplara bir çağrı ekledim . Aksi takdirde, sözlüğümdeki değerler olan tamsayılarımı alır ve istediğim gibi olmayan karma listelerini geri döndürür.
osa

Bir frozenset, UNORDERED koleksiyonudur, bu nedenle girdilerini sıralayarak kazanılacak hiçbir şey yoktur. Öte yandan, listeler ve tuples SİPARİŞ koleksiyonlardır ("diziler") ve bu nedenle karma değeri içindeki öğelerin sırasından etkilenmelidir. Onları sıralamamalısın!
RobM

14

İşte daha net bir çözüm.

def freeze(o):
  if isinstance(o,dict):
    return frozenset({ k:freeze(v) for k,v in o.items()}.items())

  if isinstance(o,list):
    return tuple([freeze(v) for v in o])

  return o


def make_hash(o):
    """
    makes a hash out of anything that contains only list,dict and hashable types including string and numeric types
    """
    return hash(freeze(o))  

Bunu değiştirirseniz if isinstance(o,list):, if isinstance(obj, (set, tuple, list)):bu işlev herhangi bir nesne üzerinde çalışabilir.
Peter Schorn

10

Aşağıdaki kod Python hash () işlevini kullanmaktan kaçınır, çünkü Python yeniden başlatmalarında tutarlı olan sağlamalar sağlamaz (Python 3.3'teki karma işlevine bakın oturumlar arasında farklı sonuçlar verir ). make_hashable()nesneyi iç içe tupllere make_hash_sha256()dönüştürür ve ayrıca repr()base64 kodlu SHA256 karmasına dönüştürür .

import hashlib
import base64

def make_hash_sha256(o):
    hasher = hashlib.sha256()
    hasher.update(repr(make_hashable(o)).encode())
    return base64.b64encode(hasher.digest()).decode()

def make_hashable(o):
    if isinstance(o, (tuple, list)):
        return tuple((make_hashable(e) for e in o))

    if isinstance(o, dict):
        return tuple(sorted((k,make_hashable(v)) for k,v in o.items()))

    if isinstance(o, (set, frozenset)):
        return tuple(sorted(make_hashable(e) for e in o))

    return o

o = dict(x=1,b=2,c=[3,4,5],d={6,7})
print(make_hashable(o))
# (('b', 2), ('c', (3, 4, 5)), ('d', (6, 7)), ('x', 1))

print(make_hash_sha256(o))
# fyt/gK6D24H9Ugexw+g3lbqnKZ0JAcgtNW+rXIDeU2Y=

1
make_hash_sha256(((0,1),(2,3)))==make_hash_sha256({0:1,2:3})==make_hash_sha256({2:3,0:1})!=make_hash_sha256(((2,3),(0,1))). Bu aradığım çözüm değil, ama güzel bir ara ürün. type(o).__name__Farklılaşmayı zorlamak için her bir tuple başlangıcını eklemeyi düşünüyorum .
Poik

Listeyi de sıralamak isterseniz:tuple(sorted((make_hashable(e) for e in o)))
Suraj

make_hash_sha256 () - güzel!
jtlz2

1
@Suraj Karma listeden önce listeyi sıralamak istememelisiniz çünkü içerikleri farklı siparişlerde olan listeler kesinlikle aynı şey değildir. Eğer öğelerin sırası sorun önemli değilse yanlış veri yapısı kullanıyor olmasıdır. Liste yerine bir küme kullanıyor olmalısınız.
scottclowe

@scottclowe Bu çok doğru. Bu noktayı eklediğiniz için teşekkürler. Hala bir liste isteyebileceğiniz 2 senaryo vardır (özel sipariş ihtiyaçları olmadan) - 1. Tekrarlanan öğelerin listesi. 2. Doğrudan bir JSON kullanmanız gerektiğinde. JSON "set" temsilini desteklemediğinden.
Suraj

5

2013 yanıtından güncellendi ...

Yukarıdaki cevapların hiçbiri benim için güvenilir görünmüyor. Nedeni () öğelerinin kullanımıdır. Bildiğim kadarıyla, bu makineye bağlı bir sırayla ortaya çıkıyor.

Buna ne dersin?

import hashlib

def dict_hash(the_dict, *ignore):
    if ignore:  # Sometimes you don't care about some items
        interesting = the_dict.copy()
        for item in ignore:
            if item in interesting:
                interesting.pop(item)
        the_dict = interesting
    result = hashlib.sha1(
        '%s' % sorted(the_dict.items())
    ).hexdigest()
    return result

Sizce neden dict.itemssıralı bir liste döndürmeyen önemli midir? frozensetilgilenir
glarrain

2
Tanım olarak bir küme sıralanmamıştır. Dolayısıyla, nesnelerin eklenme sırası önemsizdir. Sen yerleşik fonksiyon olduğunu fark var mı hashfrozenset içeriği basılı veya buna benzer bir şey nasıl umursamıyor. Birkaç makine ve python sürümünde test edin ve göreceksiniz.
glarrain

Neden değer = hash ('% s ::% s'% (değer, tür (değer))) içindeki ekstra hash () çağrısını kullanıyorsunuz?
RuiDo

4

Anahtar sırasını korumak için, yerine hash(str(dictionary))veya hash(json.dumps(dictionary))hızlı ve kirli çözümü tercih ederim:

from pprint import pformat
h = hash(pformat(dictionary))

DateTimeJSON serileştirilemeyen türler ve daha fazlası için bile çalışır .


3
Pformat veya json'un her zaman aynı siparişi kullanmasını kim garanti eder?
ThiefMaster

1
@ThiefMaster, "Sürüm 2.5'te değişti: Sözlükler, ekran hesaplanmadan önce tuşa göre sıralanıyor; 2.5'ten önce, bir sözlük yalnızca, ekranı belgelenmemiş olmasına rağmen birden fazla satır gerektiriyorsa sıralandı. " ( Docs.python. org / 2 / library / pprint.html )
Arel

2
Bu benim için geçerli görünmüyor. Pprint modülleri ve pformat yazarlar tarafından serileştirme amaçlı değil sergi amaçlı olarak anlaşılmaktadır. Bu nedenle, pformatın her zaman işe yarayacak bir sonuç getireceğini varsayarak güvende hissetmemelisiniz.
David Sanders

3

Kararınızı dondurmak ve yıkanabilir hale getirmek için üçüncü taraf frozendictmodülünü kullanabilirsiniz.

from frozendict import frozendict
my_dict = frozendict(my_dict)

Yuvalanmış nesneleri işlemek için şunları yapabilirsiniz:

import collections.abc

def make_hashable(x):
    if isinstance(x, collections.abc.Hashable):
        return x
    elif isinstance(x, collections.abc.Sequence):
        return tuple(make_hashable(xi) for xi in x)
    elif isinstance(x, collections.abc.Set):
        return frozenset(make_hashable(xi) for xi in x)
    elif isinstance(x, collections.abc.Mapping):
        return frozendict({k: make_hashable(v) for k, v in x.items()})
    else:
        raise TypeError("Don't know how to make {} objects hashable".format(type(x).__name__))

Daha fazla türü desteklemek istiyorsanız, functools.singledispatch(Python 3.7) kullanın :

@functools.singledispatch
def make_hashable(x):
    raise TypeError("Don't know how to make {} objects hashable".format(type(x).__name__))

@make_hashable.register
def _(x: collections.abc.Hashable):
    return x

@make_hashable.register
def _(x: collections.abc.Sequence):
    return tuple(make_hashable(xi) for xi in x)

@make_hashable.register
def _(x: collections.abc.Set):
    return frozenset(make_hashable(xi) for xi in x)

@make_hashable.register
def _(x: collections.abc.Mapping):
    return frozendict({k: make_hashable(v) for k, v in x.items()})

# add your own types here

Bu için, örneğin, çalışmaz dictait DataFramenesneler.
James Hirschorn

@JamesHirschorn: Yüksek sesle başarısızlık için güncellendi
Eric

Daha iyi! Ben s elifile çalışması için aşağıdaki maddeyi ekledim DataFrame: elif isinstance(x, pd.DataFrame): return make_hashable(hash_pandas_object(x).tolist()) Cevabı düzenleyecek ve kabul edip etmeyeceğinizi göreceğim ...
James Hirschorn

1
TAMAM. Görüyorum ki, sadece eşit olan nesnelerin aynı hash'e sahip olacağını garanti eden "yıkanabilir" den daha fazlasını istiyordum. Ben çalışır arasında aynı değeri verecek bir sürüm üzerinde çalışıyorum, ve python sürümü, vb bağımsız ..
James Hirschorn

1
hashrasgeleleştirme, python 3.7'de varsayılan olarak etkinleştirilen kasıtlı güvenlik özelliğidir.
Eric

1

Bunu yapmak için haritalar kitaplığını kullanabilirsiniz. Özellikle, haritalar.

import maps
fm = maps.FrozenMap(my_dict)
hash(fm)

Yüklemek mapsiçin şunları yapın:

pip install maps

Yuvalanmış dictkasayı da işler :

import maps
fm = maps.FrozenMap.recurse(my_dict)
hash(fm)

Feragatname: Ben mapskütüphanenin yazarıyım.


Kütüphane, bir kararın içindeki listeyi sıralamaz. Ve böylece bu farklı karma üretebilir. Bir listeyi sıralamak için de bir seçenek olmalıdır. Bir frozenset yardımcı olmalı, ancak bir dikte listesi içeren iç içe bir dikte ile davayı nasıl ele alacağınızı merak ediyorum. Diksiyon utanmaz gibi.
Suraj

1
@Suraj: o does aracılığıyla sap iç içe bir yapı .recurse. Bkz. Maps.readthedocs.io/en/latest/api.html#maps.FrozenMap.recurse . Listelerde sipariş vermek anlamsal olarak anlamlıdır, sipariş bağımsızlığı istiyorsanız, aramadan önce listelerinizi setlere dönüştürebilirsiniz .recurse. Bu list_fnparametreyi, (.eg ) ' .recurseden farklı bir yıkanabilir veri yapısı kullanmak için de kullanabilirsiniz.tuplefrozenset
Pedro Cattori

0

Soruna yaklaşmanın bir yolu, sözlük öğelerinin bir demetini yapmaktır:

hash(tuple(my_dict.items()))

-8

Bunu şöyle yaparım:

hash(str(my_dict))

1
Birisi bu yöntemde neyin yanlış olduğunu açıklayabilir mi?
mhristache

7
@maximi Sözlükler düzen açısından kararlı değildir, bu nedenle hash(str({'a': 1, 'b': 2})) != hash(str({'b': 2, 'a': 1}))(bazı sözlüklerde çalışabilse de, hepsinde çalışacağı garanti edilmez).
Vlad Frolov
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.