사전을 해싱?


156

캐싱을 위해 dict에 존재하는 GET 인수에서 캐시 키를 생성해야합니다.

현재 sha1(repr(sorted(my_dict.items())))( 내부적으로 hashlibsha1() 를 사용하는 편리한 방법입니다)를 사용하고 있지만 더 좋은 방법이 있는지 궁금합니다.


4
중첩 된 dict에서는 작동하지 않을 수 있습니다. 가장 짧은 해결책은 json.dumps (my_dict, sort_keys = True)를 대신 사용하는 것인데,이 값은 dict 값으로 되풀이됩니다.
Andrey Fedorov

2
참고로 재 : 덤프, stackoverflow.com/a/12739361/1082367는 피클의 출력은 DICT 및 설정하기 위해 비 결정적 인과 비슷한 이유로 정식으로 보장 할 수 없습니다 "라는 해시에 대한 피클 또는 pprint 나에 repr를 사용하지 마십시오. "
Matthew Cornell

항목이 아닌 dict 키를 정렬하고 키를 해시 함수로 보냅니다.
nyuwec

2
python.org/dev/peps/pep-0351 은 가변 데이터 구조 (사전과 같은) 해싱에 대한 흥미로운 배경 이야기로 , 임의로 객체를 고정시킬 수는 있지만 거부되었습니다. 이론적 근거는 python-dev에서 다음 스레드를 참조하십시오. mail.python.org/pipermail/python-dev/2006-February/060793.html
FluxLemur

데이터가 json 형식이고 의미 적으로 변하지 않는 해싱을 원하면 github.com/schollii/sandals/blob/master/json_sem_hash.py를 확인 하십시오 . 중첩 구조 (물론 json 이후)에서 작동하며 보존 순서 (python의 수명 동안 진화했습니다)와 같은 dict의 내부에 의존하지 않으며 두 데이터 구조가 의미 적으로 동일한 경우 동일한 해시를 제공합니다 ( like {'a': 1, 'b':2}는 의미 적으로 {'b':2, 'a':1}) 와 동일합니다 . 너무 복잡하지만 YMMV에는 사용하지 않았지만 피드백은 환영합니다.
올리버

답변:


110

사전이 중첩되어 있지 않으면 dict의 항목으로 고정 세트를 만들고 다음을 사용할 수 있습니다 hash().

hash(frozenset(my_dict.items()))

JSON 문자열 또는 사전 표현을 생성하는 것보다 계산 집약도가 훨씬 낮습니다.

업데이트 : 아래 설명을 참조하십시오.이 방법으로 안정적인 결과를 얻지 못할 수 있습니다.


9
이것은 중첩 된 사전으로 작동하지 않았습니다. 아래 해결책을 시도하지 않았습니다 (너무 복잡합니다). OP의 솔루션은 완벽하게 작동합니다. 가져 오기를 저장하기 위해 sha1을 해시로 대체했습니다.
spatel

9
@Ceaser 튜플은 순서를 의미하지만 dict 항목은 순서가 없으므로 작동하지 않습니다. 냉동 된 것이 좋습니다.
안티 모니

28
서로 다른 시스템간에 일관성이 필요한 경우 내장 해시에주의하십시오. Heroku 및 GAE와 같은 클라우드 플랫폼에서 python을 구현하면 서로 다른 인스턴스에서 hash ()에 대해 서로 다른 값을 반환하여 둘 이상의 "기계"(헤 로쿠의 경우 dynos)간에 공유해야하는 모든 항목에 대해 쓸모없는 값을 반환합니다.
Ben Roberts

6
hash()함수가 안정적인 출력을 생성하지 않는 것이 흥미로울 수 있습니다 . 이것은 동일한 입력이 주어지면 동일한 파이썬 인터프리터의 다른 인스턴스로 다른 결과를 반환한다는 것을 의미합니다. 나에게 통역사가 시작될 때마다 일종의 시드 값이 생성되는 것처럼 보입니다.
Hermann Schachner

7
예상했다. 시드는 일종의 메모리 무작위 화를 추가하는 것을 기억하는 한 보안상의 이유로 도입되었습니다. 따라서 두 파이썬 프로세스간에 해시가 동일 할 것으로 예상 할 수 없습니다.
Nikokrock

137

사용하는 sorted(d.items())것만으로는 안정적인 repr을 얻을 수 없습니다. 의 일부 값은 d사전 일 수도 있으며 해당 키는 여전히 임의의 순서로 나옵니다. 모든 키가 문자열 인 한 다음을 사용하는 것이 좋습니다.

json.dumps(d, sort_keys=True)

즉, 다른 컴퓨터 또는 Python 버전에서 해시가 안정적이어야하는 경우 이것이 방탄인지 확실하지 않습니다. 기본값으로 변경되는 것을 방지하기 위해 separatorsensure_ascii인수 를 추가 할 수 있습니다 . 의견을 부탁드립니다.


6
이것은 편집증 일뿐이지만 JSON은 대부분의 문자가 리터럴 이스케이프없이 문자열로 표시되도록 허용하므로 인코더는 문자를 이스케이프 처리할지 아니면 통과시키는 지 여부를 선택할 수 있습니다. 그러면 엔코더의 다른 버전 (또는 이후 버전)이 기본적으로 다른 이스케이프 선택을 할 수 있고 프로그램이 다른 환경에서 동일한 사전에 대해 다른 해시 값을 계산할 위험이 있습니다. ensure_ascii인수는이 완전히 가상의 문제를 방지합니다.
Jack O'Connor

4
다른 데이터 세트로 이것의 성능을 테스트했는데, 이보다 훨씬 빠릅니다 make_hash. gist.github.com/charlax/b8731de51d2ea86c6eb9
charlax

3
@charlax ujson은 dict 쌍의 순서를 보장하지 않으므로 그렇게하는 것이 안전하지 않습니다
arthurprs

11
이 솔루션은 모든 키가 문자열 인 경우에만 작동합니다 (예 : json.dumps ({ 'a': {(0, 5) : 5, 1 : 3}})).
kadee

5
@LorenzoBelli, 당신은 명령 에 추가 default=str하여 그것을 극복 할 수 있습니다 dumps. 잘 작동하는 것 같습니다.
mlissner

63

편집 : 모든 키가 문자열 인 경우이 답변을 계속 읽기 전에 Jack O'Connor의 훨씬 간단하고 빠른 솔루션 (중첩 사전 해시에도 작동)을 참조하십시오.

답변이 수락되었지만 질문의 제목은 "python 사전 해시"이며 해당 제목과 관련하여 답변이 불완전합니다. (질문과 관련하여 답변이 완료되었습니다.)

중첩 된 사전

딕셔너리를 해시하는 방법에 대해 스택 오버플로를 검색하면 제목이 지정된이 질문에 걸려 넘어지고 중첩 된 사전을 여러 번 해시하려고하면 불만족 스러울 수 있습니다. 이 경우 위의 답변이 작동하지 않으므로 해시를 검색하기 위해 일종의 재귀 메커니즘을 구현해야합니다.

다음은 그러한 메커니즘 중 하나입니다.

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()))))

보너스 : 해싱 객체 및 클래스

hash()함수는 클래스 나 인스턴스를 해시 할 때 효과적입니다. 그러나 객체와 관련하여 해시에서 발견 한 문제는 다음과 같습니다.

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

foo를 변경 한 후에도 해시는 동일합니다. 이는 foo의 ID가 변경되지 않았기 때문에 해시가 동일하기 때문입니다. 현재 정의에 따라 foo를 다르게 해시하려면 실제로 변경되는 내용을 해시하는 것이 해결책입니다. 이 경우 __dict__속성은 다음과 같습니다.

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

아아, 클래스 자체로 같은 일을하려고 할 때 :

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

클래스 __dict__속성은 일반적인 사전이 아닙니다.

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

다음은 클래스를 적절히 처리하는 이전과 비슷한 메커니즘입니다.

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()))))

이것을 사용하여 원하는 많은 요소의 해시 튜플을 반환 할 수 있습니다.

# -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__]))

참고 : 위의 모든 코드는 Python 3.x를 가정합니다. make_hash()2.7.2에서 작동 한다고 가정하지만 이전 버전에서는 테스트하지 않았습니다 . 지금까지 예제 작품을 만드는 등, 난 않는 것을 알고있다

func.__code__ 

로 교체해야합니다

func.func_code

isinstance는 두 번째 인수의 순서를 취하므로 isinstance (o, (set, tuple, list))가 작동합니다.
Xealot

frozenset가 지속적으로 querystring 매개 변수를 해시 할 수 있음을 인식하게 해주셔서 감사합니다 :)
Xealot

1
dict 항목 순서가 다르지만 키 값이 다르지 않은 경우 동일한 해시를 작성하려면 항목을 정렬해야합니다.-> 해시 리턴 (tuple (frozenset (sorted (new_o.items ()))))
Bas Koopmans

좋은! 또한 hash주변 목록과 튜플에 전화를 추가했습니다 . 그렇지 않으면 내 사전의 값으로 발생하는 정수 목록을 가져 와서 해시 목록을 반환합니다.
osa

고정 세트는 UNORDERED 모음이므로 입력을 정렬하여 얻을 수있는 것은 없습니다. 반면에 목록과 튜플은 ORDERED 모음 ( "시퀀스")이므로 해시 값은 항목 순서에 영향을받습니다. 당신은 그들을 정렬해서는 안됩니다!
RobM

14

보다 명확한 해결책이 있습니다.

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

로 변경 if isinstance(o,list):하면 if isinstance(obj, (set, tuple, list)):이 기능이 모든 객체에서 작동 할 수 있습니다.
피터 Schorn

10

아래 코드는 Python을 다시 시작할 때 일관된 해시를 제공하지 않기 때문에 Python hash () 함수를 사용하지 않습니다 ( Python 3.3의 해시 함수가 세션간에 다른 결과를 반환 함 ). make_hashable()객체를 중첩 된 튜플 make_hash_sha256()로 변환하고 repr()base64로 인코딩 된 SHA256 해시 로 변환합니다 .

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))). 이것은 내가 찾고있는 해결책이 아니지만 좋은 중간체입니다. 나는 type(o).__name__차별화를 강요하기 위해 각 튜플의 시작 부분 에 추가 하려고 생각하고 있습니다.
Poik

당신은 또한 목록을 정렬하려면 :tuple(sorted((make_hashable(e) for e in o)))
Suraj

make_hash_sha256 ()-좋습니다!
jtlz2 2009 년

1
@Suraj 서로 다른 순서로 내용이있는 목록은 확실히 같지 않기 때문에 해시하기 전에 목록을 정렬하지 말아야합니다. 항목의 순서가 중요하지 않은 경우 문제는 잘못된 데이터 구조를 사용하고 있다는 것입니다. 목록 대신 세트를 사용해야합니다.
scottclowe

@scottclowe 그건 사실입니다. 그 점을 추가해 주셔서 감사합니다. 목록을 원하는 두 가지 시나리오가 있습니다 (특정 주문 필요 없음)-1. 반복 항목 목록. 2. JSON을 직접 사용해야 할 때. JSON은 "세트"표현을 지원하지 않기 때문에.
Suraj

5

2013 년 답변에서 업데이트 ...

위의 답변 중 어느 것도 나에게 신뢰할만한 것 같지 않습니다. 그 이유는 items ()를 사용하기 때문입니다. 내가 아는 한, 이것은 기계 의존적 순서로 나옵니다.

대신 이건 어때?

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

dict.items예상대로 정렬 된 목록을 반환하지 않는 것이 왜 중요하다고 생각 하십니까? frozenset알아서
glarrain

2
정의에 따르면 세트는 순서가 없습니다. 따라서 객체가 추가되는 순서는 관련이 없습니다. 내장 함수 hash는 고정 된 내용이 인쇄되는 방식 또는 이와 유사한 것에 신경 쓰지 않는다는 것을 알아야합니다. 여러 머신과 파이썬 버전에서 테스트하면 알 수 있습니다.
glarrain

value = hash ( '% s :: % s'% (value, type (value)))에서 여분의 hash () 호출을 사용하는 이유는 무엇입니까?
RuiDo

4

키 순서를 유지하려면 대신 hash(str(dictionary))또는 더 hash(json.dumps(dictionary))빠르고 더러운 솔루션을 선호합니다.

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

DateTimeJSON 직렬화 가능하지 않은 유사 유형에 대해서도 작동 합니다.


3
pformat 또는 json이 항상 같은 순서를 사용하도록 누가 보증합니까?
ThiefMaster

1
@ThiefMaster, "버전 2.5에서 변경됨 : 사전은 키가 계산되기 전에 키를 기준으로 정렬됩니다. 2.5 이전에는 문서가 표시되지 않았지만 디스플레이에 둘 이상의 행이 필요한 경우에만 사전이 정렬되었습니다."( docs.python. org / 2 / library / pprint.html )
Arel

2
이것은 나에게 유효하지 않은 것 같습니다. pprint 모듈 및 pformat은 저자가 직렬화가 아닌 표시 목적으로 이해합니다. 이 때문에 pformat이 항상 작동하는 결과를 반환한다고 가정해도 안전하지 않습니다.
David Sanders

3

타사 frozendict모듈 을 사용하여 받아쓰기를 중지하고 해시 가능하게 만들 수 있습니다.

from frozendict import frozendict
my_dict = frozendict(my_dict)

중첩 된 객체를 처리하려면 다음을 수행하십시오.

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__))

더 많은 유형을 지원하려면 functools.singledispatch(Python 3.7)을 사용하십시오.

@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

이것은, 예를 들어, 작동하지 않습니다 dictDataFrame객체.
James Hirschorn 17 년

@JamesHirschorn : 크게 실패하도록 업데이트
Eric

보다 나은! 나는 s elif와 함께 작동하도록 다음 조항을 추가했습니다 DataFrame: elif isinstance(x, pd.DataFrame): return make_hashable(hash_pandas_object(x).tolist()) 답변을 편집하고 당신이 그것을 수락하는지 확인합니다 ...
James Hirschorn

1
확인. 필자는 "해시 가능"이상의 것을 요구하는 것을 보았습니다. 동일한 객체는 동일한 해시를 갖도록 보장합니다. 나는 런 사이에 동일한 값을 제공하고 파이썬 버전과 독립적 인 버전을 작업하고 있습니다.
James Hirschorn

1
hash무작위 화는 파이썬 3.7에서 기본적으로 활성화 된 고의적 인 보안 기능입니다.
Eric

1

이를 위해 지도 라이브러리를 사용할 수 있습니다 . 구체적으로, maps.FrozenMap

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

설치하려면 다음을 maps수행하십시오.

pip install maps

중첩 된 dict경우도 처리합니다 .

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

면책 조항 : 나는 maps도서관 의 저자입니다 .


라이브러리는 dict 내에서 목록을 정렬하지 않습니다. 따라서 다른 해시를 생성 할 수 있습니다. 목록을 정렬하는 옵션도 있어야합니다. 고정 된 세트가 도움이되지만 dict 목록이 포함 된 중첩 된 dict로 사건을 어떻게 처리 할 수 ​​있을지 궁금합니다. 받아쓰기로 해싱 할 수 없습니다.
Suraj

1
@Suraj : 그것은 수행 을 통해 핸들 중첩 된 구조를 .recurse. maps.readthedocs.io/en/latest/api.html#maps.FrozenMap.recurse를 참조하십시오 . 목록의 순서는 의미 상 의미가 있습니다. 순서 독립성을 원하면을 호출하기 전에 목록을 세트로 변환 할 수 있습니다 .recurse. 당신은 또한 사용할 수 있습니다 list_fn하는 매개 변수 .recurse와 다른 해쉬 데이터 구조를 사용하는 tuple(.eg frozenset)
페드로 Cattori

0

문제에 접근하는 한 가지 방법은 사전 항목의 튜플을 만드는 것입니다.

hash(tuple(my_dict.items()))

-8

나는 이렇게한다 :

hash(str(my_dict))

1
누군가이 방법으로 무엇이 잘못되었는지 설명 할 수 있습니까?
mhristache

7
@maximi 사전은 순서가 안정적이지 않으므로 hash(str({'a': 1, 'b': 2})) != hash(str({'b': 2, 'a': 1}))(일부 사전에서는 작동하지만 모두 작동하지는 않습니다.)
블라드 프롤로 프
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.