복붙노트

[PYTHON] 파이썬 해시 가능 딕테이션

PYTHON

파이썬 해시 가능 딕테이션

연습으로, 그리고 주로 내 자신의 즐거움을 위해서, 나는 백 트랙킹 팩트 파서를 구현하고있다. 이것에 대한 영감은 유전자 변형 매크로가 algol-like 언어에서 어떻게 작동하는지에 대해 더 잘 알고 싶습니다. (일반적으로 당신이 사용하는 syntax free lisp 방언과 유사합니다.) 이 때문에 입력을 통해 다른 패스가 다른 문법을 볼 수 있으므로 캐시 된 구문 분석 결과와 함께 현재 버전의 문법도 저장하지 않으면 캐시 된 구문 분석 결과가 유효하지 않습니다. (편집 :이 키 - 값 컬렉션의 사용의 결과는 그들이 불변해야한다는 것입니다,하지만 나는 변경할 수 있도록 인터페이스를 폭로하지 않으므로, 변경 또는 불변 컬렉션 중 하나입니다)

문제는 파이썬 사전이 다른 사전에 키로 나타날 수 없다는 것입니다. 튜플을 사용하더라도 (어쨌든 내가하고있는 것처럼) 도움이되지 않는다.

>>> cache = {}
>>> rule = {"foo":"bar"}
>>> cache[(rule, "baz")] = "quux"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'dict'
>>> 

나는 튜플이 끝까지 내려와 있어야한다고 생각한다. 이제 파이썬 표준 라이브러리는 대략 필요한 것, collections.namedtuple은 매우 다른 구문을 제공하지만 키로 사용할 수 있습니다. 상기 세션에서 계속 :

>>> from collections import namedtuple
>>> Rule = namedtuple("Rule",rule.keys())
>>> cache[(Rule(**rule), "baz")] = "quux"
>>> cache
{(Rule(foo='bar'), 'baz'): 'quux'}

승인. 그러나 각 규칙은 사용하는 매개 변수를 정확히 알고 있기 때문에 클래스를 동시에 정의 할 수 있기 때문에 사용하고 싶은 규칙에서 가능한 모든 키 조합에 대해 클래스를 만들어야합니다. 규칙을 구문 분석하는 함수로

편집 : 명명 된 튜플의 또 다른 문제점은 엄격하게 위치 지정된다는 것입니다. 서로 달라야하는 것처럼 보이는 두 개의 튜플은 실제로 동일 할 수 있습니다.

>>> you = namedtuple("foo",["bar","baz"])
>>> me = namedtuple("foo",["bar","quux"])
>>> you(bar=1,baz=2) == me(bar=1,quux=2)
True
>>> bob = namedtuple("foo",["baz","bar"])
>>> you(bar=1,baz=2) == bob(bar=1,baz=2)
False

tl'dr : 다른 dicts에 대한 키로 사용될 수있는 dicts를 얻으려면 어떻게해야합니까?

해답을 조금씩 해킹 한 결과, 여기에 제가 사용하고있는보다 완벽한 해결책이 있습니다. 그 결과 dicts을 실제 목적을 위해 막연하게 불변으로 만드는 약간의 추가 작업을 수행합니다. 물론 dict .__ setitem __ (instance, key, value)를 호출하여 해킹하는 것은 여전히 ​​쉽지만 우리는 모두 성인입니다.

class hashdict(dict):
    """
    hashable dict implementation, suitable for use as a key into
    other dicts.

        >>> h1 = hashdict({"apples": 1, "bananas":2})
        >>> h2 = hashdict({"bananas": 3, "mangoes": 5})
        >>> h1+h2
        hashdict(apples=1, bananas=3, mangoes=5)
        >>> d1 = {}
        >>> d1[h1] = "salad"
        >>> d1[h1]
        'salad'
        >>> d1[h2]
        Traceback (most recent call last):
        ...
        KeyError: hashdict(bananas=3, mangoes=5)

    based on answers from
       http://stackoverflow.com/questions/1151658/python-hashable-dicts

    """
    def __key(self):
        return tuple(sorted(self.items()))
    def __repr__(self):
        return "{0}({1})".format(self.__class__.__name__,
            ", ".join("{0}={1}".format(
                    str(i[0]),repr(i[1])) for i in self.__key()))

    def __hash__(self):
        return hash(self.__key())
    def __setitem__(self, key, value):
        raise TypeError("{0} does not support item assignment"
                         .format(self.__class__.__name__))
    def __delitem__(self, key):
        raise TypeError("{0} does not support item assignment"
                         .format(self.__class__.__name__))
    def clear(self):
        raise TypeError("{0} does not support item assignment"
                         .format(self.__class__.__name__))
    def pop(self, *args, **kwargs):
        raise TypeError("{0} does not support item assignment"
                         .format(self.__class__.__name__))
    def popitem(self, *args, **kwargs):
        raise TypeError("{0} does not support item assignment"
                         .format(self.__class__.__name__))
    def setdefault(self, *args, **kwargs):
        raise TypeError("{0} does not support item assignment"
                         .format(self.__class__.__name__))
    def update(self, *args, **kwargs):
        raise TypeError("{0} does not support item assignment"
                         .format(self.__class__.__name__))
    # update is not ok because it mutates the object
    # __add__ is ok because it creates a new object
    # while the new object is under construction, it's ok to mutate it
    def __add__(self, right):
        result = hashdict(self)
        dict.update(result, right)
        return result

if __name__ == "__main__":
    import doctest
    doctest.testmod()

해결법

  1. ==============================

    1.해시 가능 사전을 만드는 쉬운 방법은 다음과 같습니다. 확실한 이유로 다른 사전에 삽입 한 후 변형시키지 마십시오.

    해시 가능 사전을 만드는 쉬운 방법은 다음과 같습니다. 확실한 이유로 다른 사전에 삽입 한 후 변형시키지 마십시오.

    class hashabledict(dict):
        def __hash__(self):
            return hash(tuple(sorted(self.items())))
    
  2. ==============================

    2.Hashables는 불변이어야한다. - 이것을 강요하지는 말고, 처음으로 키로 사용한 후에는 dict을 변형시키지 말라. 다음과 같은 접근 방식이 가능할 것이다 :

    Hashables는 불변이어야한다. - 이것을 강요하지는 말고, 처음으로 키로 사용한 후에는 dict을 변형시키지 말라. 다음과 같은 접근 방식이 가능할 것이다 :

    class hashabledict(dict):
      def __key(self):
        return tuple((k,self[k]) for k in sorted(self))
      def __hash__(self):
        return hash(self.__key())
      def __eq__(self, other):
        return self.__key() == other.__key()
    

    당신의 딕트를 돌연변이 할 필요가 있고 여전히 그것들을 키로 사용하기를 원한다면, 복잡성은 백 배가됩니다. 말할 수는 없지만 그 놀라운 털갈이에 들어가기 전에 나는 아주 구체적인 표시가 나타날 때까지 기다릴 것입니다! -)

  3. ==============================

    3.사전을 목적에 맞게 사용하는 데 필요한 모든 것은 __hash__ 메소드를 추가하는 것입니다.

    사전을 목적에 맞게 사용하는 데 필요한 모든 것은 __hash__ 메소드를 추가하는 것입니다.

    class Hashabledict(dict):
        def __hash__(self):
            return hash(frozenset(self))
    

    모든 사전에 대해 frozenset 변환이 가능합니다 (즉, 키를 정렬 할 필요가 없음). 마찬가지로, 사전 값에는 제한이 없습니다.

    동일한 키가 있지만 값이 다른 사전이 많은 경우 해시에 값을 고려해야합니다. 가장 빠른 방법은 다음과 같습니다.

    class Hashabledict(dict):
        def __hash__(self):
            return hash((frozenset(self), frozenset(self.itervalues())))
    

    이것은 두 가지 이유로 frozenset (self.iteritems ())보다 빠릅니다. 첫째, frozenset (self) 단계는 사전에 저장된 해시 값을 다시 사용하여 해시 (key)에 불필요한 호출을 저장합니다. 둘째, itervalues를 사용하면 직접 값에 액세스 할 수 있으며 조회를 할 때마다 메모리에 새로운 많은 키 / 값 튜플을 형성하기 위해 항목별로 사용하는 많은 메모리 할당 자 호출을 피할 수 있습니다.

  4. ==============================

    4.주어진 답변은 괜찮지 만 튜플 (sorted (...)) 대신에 frozenset (...)을 사용하여 해시를 생성하여 향상시킬 수 있습니다.

    주어진 답변은 괜찮지 만 튜플 (sorted (...)) 대신에 frozenset (...)을 사용하여 해시를 생성하여 향상시킬 수 있습니다.

    >>> import timeit
    >>> timeit.timeit('hash(tuple(sorted(d.iteritems())))', "d = dict(a=3, b='4', c=2345, asdfsdkjfew=0.23424, x='sadfsadfadfsaf')")
    4.7758948802947998
    >>> timeit.timeit('hash(frozenset(d.iteritems()))', "d = dict(a=3, b='4', c=2345, asdfsdkjfew=0.23424, x='sadfsadfadfsaf')")
    1.8153600692749023
    

    성능 이점은 사전의 내용에 달려 있지만, 대부분 테스트 한 결과, frozenset의 해시는 적어도 2 배 더 빠릅니다 (주로 정렬 할 필요가 없기 때문에).

  5. ==============================

    5.합리적으로 깨끗하고 간단하게 구현할 수 있습니다.

    합리적으로 깨끗하고 간단하게 구현할 수 있습니다.

    import collections
    
    class FrozenDict(collections.Mapping):
        """Don't forget the docstrings!!"""
    
        def __init__(self, *args, **kwargs):
            self._d = dict(*args, **kwargs)
    
        def __iter__(self):
            return iter(self._d)
    
        def __len__(self):
            return len(self._d)
    
        def __getitem__(self, key):
            return self._d[key]
    
        def __hash__(self):
            return hash(tuple(sorted(self._d.iteritems())))
    
  6. ==============================

    6.나는 계속이 주제로 돌아 간다 ... 또 다른 변형이있다. 나는 __hash__ 메쏘드를 추가하기 위해 dict를 하위 클래스 화하는 것이 쉽지 않다. dict의 변경이 가능하고 변경되지 않는다고 믿는 것은 약한 아이디어처럼 보입니다. 그래서 나는 그 자체로 불변 인 내장 타입에 기반한 매핑을 만드는 것을 보았습니다. 튜플은 명백한 선택이지만, 값을 액세스하는 것은 일종의 양분을 의미한다. 문제는 아니지만 그것이 구축 한 유형의 힘을 최대한 활용하는 것 같지 않습니다.

    나는 계속이 주제로 돌아 간다 ... 또 다른 변형이있다. 나는 __hash__ 메쏘드를 추가하기 위해 dict를 하위 클래스 화하는 것이 쉽지 않다. dict의 변경이 가능하고 변경되지 않는다고 믿는 것은 약한 아이디어처럼 보입니다. 그래서 나는 그 자체로 불변 인 내장 타입에 기반한 매핑을 만드는 것을 보았습니다. 튜플은 명백한 선택이지만, 값을 액세스하는 것은 일종의 양분을 의미한다. 문제는 아니지만 그것이 구축 한 유형의 힘을 최대한 활용하는 것 같지 않습니다.

    열쇠, 가치 쌍을 frozenset에 넣으면 어떨까요? 그것이 필요한 것은 무엇이며 어떻게 작동할까요?

    1 부, 당신은 'frozenset이 키에 의해 주로 그들을 다룰 것 같은 방식으로'항목을 인코딩하는 방법이 필요합니다. 나는 그것을 위해 약간의 서브 클래스를 만들 것이다.

    import collections
    class pair(collections.namedtuple('pair_base', 'key value')):
        def __hash__(self):
            return hash((self.key, None))
        def __eq__(self, other):
            if type(self) != type(other):
                return NotImplemented
            return self.key == other.key
        def __repr__(self):
            return repr((self.key, self.value))
    

    이것만으로도 불변의 맵핑이 가능합니다.

    >>> frozenset(pair(k, v) for k, v in enumerate('abcd'))
    frozenset([(0, 'a'), (2, 'c'), (1, 'b'), (3, 'd')])
    >>> pairs = frozenset(pair(k, v) for k, v in enumerate('abcd'))
    >>> pair(2, None) in pairs
    True
    >>> pair(5, None) in pairs
    False
    >>> goal = frozenset((pair(2, None),))
    >>> pairs & goal
    frozenset([(2, None)])
    

    D' oh! 불행히도 집합 연산자를 사용할 때 요소는 동일하지만 같은 객체는 아닙니다. 어느 쪽이 반환 값으로 끝나는지는 정의되어 있지 않습니다. 우리는 더 많은 선회를해야 할 것입니다.

    >>> pairs - (pairs - goal)
    frozenset([(2, 'c')])
    >>> iter(pairs - (pairs - goal)).next().value
    'c'
    

    그러나 이러한 방식으로 값을 찾는 것은 번거롭고, 더 나쁜 경우 많은 중간 세트를 만듭니다. 그럴 수 없어! 우리는 주위를 둘러보기 위해 '가짜'키 - 값 쌍을 만듭니다.

    class Thief(object):
        def __init__(self, key):
            self.key = key
        def __hash__(self):
            return hash(pair(self.key, None))
        def __eq__(self, other):
            self.value = other.value
            return pair(self.key, None) == other
    

    덜 문제가되는 결과는 다음과 같습니다.

    >>> thief = Thief(2)
    >>> thief in pairs
    True
    >>> thief.value
    'c'
    

    그것은 모두 깊은 마법입니다. 나머지는 그 모든 것을 위와 같은 인터페이스를 가진 무언가로 싸고있다. 우리는 매우 다른 인터페이스를 가진 frozenset으로부터 서브 클래 싱을하기 때문에 꽤 많은 메소드가 있습니다. 우리는 콜렉션에서 약간의 도움을 얻습니다. 매핑. 그러나 대부분의 작업은 대신 dicts처럼 작동하는 버전의 frozenset 메소드를 오버라이드합니다.

    class FrozenDict(frozenset, collections.Mapping):
        def __new__(cls, seq=()):
            return frozenset.__new__(cls, (pair(k, v) for k, v in seq))
        def __getitem__(self, key):
            thief = Thief(key)
            if frozenset.__contains__(self, thief):
                return thief.value
            raise KeyError(key)
        def __eq__(self, other):
            if not isinstance(other, FrozenDict):
                return dict(self.iteritems()) == other
            if len(self) != len(other):
                return False
            for key, value in self.iteritems():
                try:
                    if value != other[key]:
                        return False
                except KeyError:
                    return False
            return True
        def __hash__(self):
            return hash(frozenset(self.iteritems()))
        def get(self, key, default=None):
            thief = Thief(key)
            if frozenset.__contains__(self, thief):
                return thief.value
            return default
        def __iter__(self):
            for item in frozenset.__iter__(self):
                yield item.key
        def iteritems(self):
            for item in frozenset.__iter__(self):
                yield (item.key, item.value)
        def iterkeys(self):
            for item in frozenset.__iter__(self):
                yield item.key
        def itervalues(self):
            for item in frozenset.__iter__(self):
                yield item.value
        def __contains__(self, key):
            return frozenset.__contains__(self, pair(key, None))
        has_key = __contains__
        def __repr__(self):
            return type(self).__name__ + (', '.join(repr(item) for item in self.iteritems())).join('()')
        @classmethod
        def fromkeys(cls, keys, value=None):
            return cls((key, value) for key in keys)
    

    궁극적으로 내 자신의 질문에 대답합니다.

    >>> myDict = {}
    >>> myDict[FrozenDict(enumerate('ab'))] = 5
    >>> FrozenDict(enumerate('ab')) in myDict
    True
    >>> FrozenDict(enumerate('bc')) in myDict
    False
    >>> FrozenDict(enumerate('ab', 3)) in myDict
    False
    >>> myDict[FrozenDict(enumerate('ab'))]
    5
    
  7. ==============================

    7.허용 된 @Unknown의 응답과 @AlexMartelli의 응답은 완벽하게 정상적으로 작동하지만 다음 제약 조건 하에서 만 작동합니다.

    허용 된 @Unknown의 응답과 @AlexMartelli의 응답은 완벽하게 정상적으로 작동하지만 다음 제약 조건 하에서 만 작동합니다.

    @ ObenSonne에 의한 훨씬 빠른 대답은 제약 2와 제약 3을 들었지만 제약 1 (값은 해시 가능해야 함)에 여전히 묶여 있습니다.

    @RaymondHettinger가 대답하는 해쉬 계산에서 .values ​​()를 포함하지 않기 때문에 더 빠르고 응답이 빠릅니다. 그러나 다음과 같은 경우에만 성능이 좋습니다.

    이 조건이 충족되지 않으면 해시 함수는 계속 유효하지만 너무 많은 충돌이 발생할 수 있습니다. 예를 들어 모든 사전이 웹 사이트 템플릿 (키로 필드 이름, 값으로 사용자 입력)에서 생성되는 극단적 인 경우 키는 항상 같고 해시 함수는 모든 입력에 대해 동일한 값을 반환합니다 . 결과적으로 해시 함수에 의존하는 해시 테이블은 항목 (O (1) 대신 O (N))을 검색 할 때 목록만큼 느려집니다.

    위의 4 가지 제약 조건을 모두 위반하더라도 다음 솔루션이 상당히 잘 작동한다고 생각합니다. 사전에 중첩 된 변경 가능한 컨테이너가 있더라도 해시 할 수있는 장점이 있습니다.

    지금까지 가볍게 테스트 한 이후로 나는 이것에 대한 피드백을 많이 주셔서 감사합니다.

    # python 3.4
    import collections
    import operator
    import sys
    import itertools
    import reprlib
    
    # a wrapper to make an object hashable, while preserving equality
    class AutoHash:
        # for each known container type, we can optionally provide a tuple
        # specifying: type, transform, aggregator
        # even immutable types need to be included, since their items
        # may make them unhashable
    
        # transformation may be used to enforce the desired iteration
        # the result of a transformation must be an iterable
        # default: no change; for dictionaries, we use .items() to see values
    
        # usually transformation choice only affects efficiency, not correctness
    
        # aggregator is the function that combines all items into one object
        # default: frozenset; for ordered containers, we can use tuple
    
        # aggregator choice affects both efficiency and correctness
        # e.g., using a tuple aggregator for a set is incorrect,
        # since identical sets may end up with different hash values
        # frozenset is safe since at worst it just causes more collisions
        # unfortunately, no collections.ABC class is available that helps
        # distinguish ordered from unordered containers
        # so we need to just list them out manually as needed
    
        type_info = collections.namedtuple(
            'type_info',
            'type transformation aggregator')
    
        ident = lambda x: x
        # order matters; first match is used to handle a datatype
        known_types = (
            # dict also handles defaultdict
            type_info(dict, lambda d: d.items(), frozenset), 
            # no need to include set and frozenset, since they are fine with defaults
            type_info(collections.OrderedDict, ident, tuple),
            type_info(list, ident, tuple),
            type_info(tuple, ident, tuple),
            type_info(collections.deque, ident, tuple),
            type_info(collections.Iterable, ident, frozenset) # other iterables
        )
    
        # hash_func can be set to replace the built-in hash function
        # cache can be turned on; if it is, cycles will be detected,
        # otherwise cycles in a data structure will cause failure
        def __init__(self, data, hash_func=hash, cache=False, verbose=False):
            self._data=data
            self.hash_func=hash_func
            self.verbose=verbose
            self.cache=cache
            # cache objects' hashes for performance and to deal with cycles
            if self.cache:
                self.seen={}
    
        def hash_ex(self, o):
            # note: isinstance(o, Hashable) won't check inner types
            try:
                if self.verbose:
                    print(type(o),
                        reprlib.repr(o),
                        self.hash_func(o),
                        file=sys.stderr)
                return self.hash_func(o)
            except TypeError:
                pass
    
            # we let built-in hash decide if the hash value is worth caching
            # so we don't cache the built-in hash results
            if self.cache and id(o) in self.seen:
                return self.seen[id(o)][0] # found in cache
    
            # check if o can be handled by decomposing it into components
            for typ, transformation, aggregator in AutoHash.known_types:
                if isinstance(o, typ):
                    # another option is:
                    # result = reduce(operator.xor, map(_hash_ex, handler(o)))
                    # but collisions are more likely with xor than with frozenset
                    # e.g. hash_ex([1,2,3,4])==0 with xor
    
                    try:
                        # try to frozenset the actual components, it's faster
                        h = self.hash_func(aggregator(transformation(o)))
                    except TypeError:
                        # components not hashable with built-in;
                        # apply our extended hash function to them
                        h = self.hash_func(aggregator(map(self.hash_ex, transformation(o))))
                    if self.cache:
                        # storing the object too, otherwise memory location will be reused
                        self.seen[id(o)] = (h, o)
                    if self.verbose:
                        print(type(o), reprlib.repr(o), h, file=sys.stderr)
                    return h
    
            raise TypeError('Object {} of type {} not hashable'.format(repr(o), type(o)))
    
        def __hash__(self):
            return self.hash_ex(self._data)
    
        def __eq__(self, other):
            # short circuit to save time
            if self is other:
                return True
    
            # 1) type(self) a proper subclass of type(other) => self.__eq__ will be called first
            # 2) any other situation => lhs.__eq__ will be called first
    
            # case 1. one side is a subclass of the other, and AutoHash.__eq__ is not overridden in either
            # => the subclass instance's __eq__ is called first, and we should compare self._data and other._data
            # case 2. neither side is a subclass of the other; self is lhs
            # => we can't compare to another type; we should let the other side decide what to do, return NotImplemented
            # case 3. neither side is a subclass of the other; self is rhs
            # => we can't compare to another type, and the other side already tried and failed;
            # we should return False, but NotImplemented will have the same effect
            # any other case: we won't reach the __eq__ code in this class, no need to worry about it
    
            if isinstance(self, type(other)): # identifies case 1
                return self._data == other._data
            else: # identifies cases 2 and 3
                return NotImplemented
    
    d1 = {'a':[1,2], 2:{3:4}}
    print(hash(AutoHash(d1, cache=True, verbose=True)))
    
    d = AutoHash(dict(a=1, b=2, c=3, d=[4,5,6,7], e='a string of chars'),cache=True, verbose=True)
    print(hash(d))
    
  8. ==============================

    8.v2 pickling 프로토콜이 hashdict 인스턴스와 작동하도록하려면이 두 가지 메소드를 추가해야 할 수도 있습니다. 그렇지 않으면 cPickle은 TypeError를 발생시키는 hashdict .____ setitem____을 사용하려고 시도합니다. 흥미롭게도 프로토콜의 다른 두 버전에서는 코드가 잘 작동합니다.

    v2 pickling 프로토콜이 hashdict 인스턴스와 작동하도록하려면이 두 가지 메소드를 추가해야 할 수도 있습니다. 그렇지 않으면 cPickle은 TypeError를 발생시키는 hashdict .____ setitem____을 사용하려고 시도합니다. 흥미롭게도 프로토콜의 다른 두 버전에서는 코드가 잘 작동합니다.

    def __setstate__(self, objstate):
        for k,v in objstate.items():
            dict.__setitem__(self,k,v)
    def __reduce__(self):
        return (hashdict, (), dict(self),)
    
  9. ==============================

    9.사전에 숫자를 입력하지 않고 사전이 포함 된 변수를 잃지 않으면 다음을 수행 할 수 있습니다.

    사전에 숫자를 입력하지 않고 사전이 포함 된 변수를 잃지 않으면 다음을 수행 할 수 있습니다.

    캐시 [아이디 (규칙)] = "뭐든지"

    id ()는 모든 사전에 대해 고유하므로

    편집하다:

    오, 죄송합니다. 다른 사람들이 말한 것이 더 낳을 것입니다. 나는 또한 당신이 사전처럼 문자열을 직렬화 할 수 있다고 생각한다.

    캐시 [ 'fu : bar'] = '번개'

    그래도 열쇠에서 사전을 복구해야한다면, 다음과 같이 더 못된 일을해야 할 것입니다.

    캐시 [ 'fu : bar'] = ({ 'fu :'bar '},'번개 ')

    이 코드의 장점은 많은 코드를 작성할 필요가 없다는 것입니다.

  10. from https://stackoverflow.com/questions/1151658/python-hashable-dicts by cc-by-sa and MIT license