Python hashable dyktu


94

Jako ćwiczenie, a przede wszystkim dla własnej rozrywki, implementuję parser z wycofywaniem się. Inspiracją do tego jest to, że chciałbym mieć lepsze wyobrażenie o tym, jak higieniczne makra będą działać w języku podobnym do algolu (w odniesieniu do dialektów lisp wolnych od składni, w których zwykle je znajdziesz). Z tego powodu różne przejścia przez dane wejściowe mogą widzieć różne gramatyki, więc buforowane wyniki analizy są nieprawidłowe, chyba że przechowuję również bieżącą wersję gramatyki wraz z buforowanymi wynikami analizy. ( EDYCJA : konsekwencją takiego użycia kolekcji klucz-wartość jest to, że powinny one być niezmienne, ale nie zamierzam ujawniać interfejsu, aby umożliwić ich zmianę, więc kolekcje zmienne lub niezmienne są w porządku)

Problem polega na tym, że dykty w Pythonie nie mogą pojawiać się jako klucze do innych dykt. Nawet użycie krotki (tak jak bym to robił) nie pomaga.

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

Myślę, że muszą to być krotki aż do końca. Teraz standardowa biblioteka Pythona zapewnia w przybliżeniu to, czego potrzebuję, collections.namedtuplema zupełnie inną składnię, ale może być używana jako klucz. kontynuacja z powyższej sesji:

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

Ok. Ale muszę stworzyć klasę dla każdej możliwej kombinacji kluczy w regule, której chciałbym użyć, co nie jest takie złe, ponieważ każda reguła analizy wie dokładnie, jakich parametrów używa, aby klasa mogła być zdefiniowana w tym samym czasie jako funkcja analizująca regułę.

Edycja: Dodatkowym problemem z namedtuples jest to, że są ściśle pozycyjne. Dwie krotki, które wyglądają na różne, mogą w rzeczywistości być takie same:

>>> 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: Jak uzyskać pliki, dictktóre mogą być używane jako klucze do innych dict?

Po zhakowaniu odpowiedzi, oto bardziej kompletne rozwiązanie, którego używam. Zauważ, że wymaga to trochę więcej pracy, aby wynikowe polecenia były niejasno niezmienne ze względów praktycznych. Oczywiście nadal dość łatwo jest zhakować to, dzwoniąc, dict.__setitem__(instance, key, value)ale wszyscy jesteśmy tutaj dorośli.

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

hashdictMuszą być niezmienne, przynajmniej po uruchomieniu mieszania, więc dlaczego nie buforują keyi hashwartości jako atrybuty hashdictobiektu? Zmodyfikowałem __key()i __hash__()przetestowałem, aby potwierdzić, że jest znacznie szybszy. SO nie zezwala na sformatowany kod w komentarzach, więc umieszczę link tutaj: sam.aiki.info/hashdict.py
Sam Watkins

Odpowiedzi:


71

Oto prosty sposób na utworzenie słownika z funkcją skrótu. Pamiętaj tylko, aby z oczywistych powodów nie modyfikować ich po osadzeniu w innym słowniku.

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

7
Nie zapewnia to ostro spójności eq i hash, podczas gdy moja wcześniejsza odpowiedź działa przy użyciu metody __key (w praktyce każde podejście powinno działać, chociaż to można spowolnić tworząc niepotrzebną listę itermediate - naprawialną przez s / items / iteritems / - zakładając Python 2. * jak nie mówisz ;-).
Alex Martelli,

5
Prawdopodobnie lepiej byłoby po prostu użyć zamrożonego zestawu zamiast krotki z sortowaniem. Nie tylko byłoby to szybsze, ale nie można założyć, że klucze słownika są porównywalne.
asmeurer

1
Wydaje się, że powinien być sposób na uniknięcie funkcji skrótu, w O(n*log(n))której nznajduje się liczba dictwpisów. Czy ktoś wie, czy frozensetfunkcja skrótu Pythona działa w czasie liniowym?
Tom Karzes

2
@HelloGoodbye Dykt może być również utworzony w ten dict(key1=value1, key2=value2,...)lub inny sposób dict([(key1, value1), (key2, value2),...)]). To samo dotyczy tego. Kreacja, którą opublikowałeś, nazywa się dosłownie
smido

2
@smido: Dzięki. Odkryłem też, że możesz po prostu rzucić dosłowny, tj hashabledict({key_a: val_a, key_b: val_b, ...}).
HelloGoodbye

62

Hashables powinny być niezmienne - nie wymuszając tego, ale ZAUFAJĄC, że nie mutujesz dyktu po jego pierwszym użyciu jako klucza, zadziałałoby następujące podejście:

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

Jeśli MUSISZ zmutować swoje dykty i NADAL chcesz ich używać jako kluczy, złożoność eksploduje stokrotnie - nie mówiąc, że nie da się tego zrobić, ale poczekam do BARDZO konkretnego wskazania, zanim wejdę w to niesamowite grzęzawisko! -)


Z pewnością nie chcę modyfikować nakazów, gdy zostały już przygotowane. To spowodowałoby rozpad reszty algorytmu packrad.
SingleNegationElimination

Wtedy podklasa, którą zasugerowałem, będzie działać - zwróć uwagę, jak omija problem "pozycyjny" ( zanim zredagowałeś swoje pytanie, aby je wskazać ;-) z sortedklawiszem in __key ;-).
Alex Martelli,

Zależne od stanowiska zachowanie imiennego dwójki zaskoczyło mnie do cholery. Bawiłem się tym, myśląc, że może to być łatwiejszy sposób rozwiązania problemu, ale to prawie przekreśliło wszystkie moje nadzieje (i będzie wymagało wycofania :()
SingleNegationElimination

Powiedzmy, że mam dyktando i chcę go przesłać do elementu z hasłem. Jak miałbym to zrobić?
jononomo


32

Wszystko, co jest potrzebne, aby słowniki były użyteczne w Twoim celu, to dodanie metody __hash__:

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

Uwaga, konwersja zestawu zamrożonego będzie działać dla wszystkich słowników (tj. Nie wymaga sortowania kluczy). Podobnie nie ma ograniczeń co do wartości słownikowych.

Jeśli istnieje wiele słowników z identycznymi kluczami, ale z różnymi wartościami, konieczne jest, aby hash uwzględniał te wartości. Najszybszym sposobem na to jest:

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

Jest to szybsze niż frozenset(self.iteritems())z dwóch powodów. Po pierwsze, frozenset(self)krok ponownie wykorzystuje wartości skrótu przechowywane w słowniku, oszczędzając niepotrzebne wywołania hash(key). Po drugie, użycie wartości itervalues zapewni bezpośredni dostęp do wartości i pozwoli uniknąć wielu wywołań alokatora pamięci używających przez elementy do tworzenia wielu nowych krotek klucz / wartość w pamięci za każdym razem, gdy przeprowadzasz wyszukiwanie.


@RaymondHettinger Popraw mnie, jeśli się mylę, ale myślałem dict, że sam nie buforuje wartości skrótu swoich kluczy - chociaż poszczególne klasy (takie jak str) mogą i decydują się buforować swoje skróty. Przynajmniej kiedy tworzyłem a dictz własnymi instancjami klasy używanymi jako klucze, ich __hash__metody były wywoływane przy każdej operacji dostępu (python 3.4). Niezależnie od tego, czy mam rację, czy nie, nie jestem pewien, jak hash(frozenset(self))mogę ponownie wykorzystać wstępnie obliczone wartości skrótu, chyba że są one przechowywane w samych kluczach (w takim przypadku również hash(frozenset(self.items())ich ponownie używa).
maksymalnie

Jeśli chodzi o drugą kwestię dotyczącą tworzenia krotek (klucz / wartość), pomyślałem, że metody .items () zwracają raczej widok niż listę krotek, a tworzenie tego widoku nie wymaga kopiowania podstawowych kluczy i wartości. (Znowu Python 3.4). To powiedziawszy, widzę zaletę haszowania tylko kluczy, jeśli większość danych wejściowych ma różne klucze - ponieważ (1) jest to dość kosztowne do wartości skrótu, a (2) wymaganie, aby wartości były hashowane, jest dość restrykcyjne
maksymalnie

6
Daje to również możliwość utworzenia tego samego skrótu dla dwóch różnych słowników. Rozważ {'one': 1, 'two': 2}i{'one': 2, 'two': 1}
AgDude

Mike Graham w swoim komentarzu stwierdza, że Deriving dyktuje z innego powodu niż zdefiniowanie, __missing__to zły pomysł. Co myślisz?
Piotr Dobrogost

1
Podklasy z dict zostały dobrze zdefiniowane od Pythona 2.2. Zobacz collections.OrderedDict i collections.Counter, aby zapoznać się z przykładami z biblioteki standardowej języka Python. Drugi komentarz jest oparty na nieuzasadnionym przekonaniu, że tylko podklasy MutableMapping są dobrze zdefiniowane.
Raymond Hettinger

23

Podane odpowiedzi są w porządku, ale można je poprawić, używając frozenset(...)zamiast tuple(sorted(...))generowania skrótu:

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

Przewaga wydajności zależy od zawartości słownika, ale w większości przypadków, które testowałem, haszowanie frozensetjest co najmniej 2 razy szybsze (głównie dlatego, że nie ma potrzeby sortowania).


1
Uwaga, nie ma potrzeby uwzględniania zarówno kluczy, jak i wartości. Takie rozwiązanie byłoby znacznie szybciej: hash(frozenset(d)).
Raymond Hettinger

10
@RaymondHettinger: hash(frozenset(d))wyniki w identycznych hashach dla 2 dykt z podobnymi kluczami, ale różnymi wartościami!
Oben Sonne

4
To nie jest problem. Zadaniem __eq__ jest rozróżnianie nakazów o różnych wartościach. Zadanie __hash__ polega jedynie na zmniejszeniu przestrzeni wyszukiwania.
Raymond Hettinger

5
Dotyczy to teoretycznej koncepcji skrótów i mapowań, ale nie jest praktyczne w przypadku pamięci podręcznych ze słownikami jako odnośnikami - nierzadko zdarza się, że słowniki z podobnymi kluczami, ale różnymi wartościami są przekazywane do funkcji buforowanej przez pamięć. W takim przypadku pamięć podręczna praktycznie zamienia się w listę zamiast w mapowanie, jeśli tylko klucze są używane do budowania skrótu.
Oben Sonne

3
W szczególnym przypadku dykt z kluczami indentycznymi i różnymi wartościami lepiej byłoby po prostu przechowywać skrót na podstawie frozenset(d.itervalues()). W przypadkach, gdy dyktowanie ma odrębne klucze, frozenset(d)jest znacznie szybsze i nie nakłada żadnych ograniczeń na haszowanie kluczy. Na koniec pamiętaj, że metoda dict .__ eq__ sprawdzi, czy pary klucz / wartość są równe, znacznie szybciej, czy cokolwiek może obliczyć hash dla wszystkich krotek par klucz / wartość. Używanie krotek klucz / wartość jest również problematyczne, ponieważ wyrzuca przechowywane skróty dla wszystkich kluczy (dlatego frozenset(d)jest tak szybkie).
Raymond Hettinger

11

Dość czysta, prosta implementacja to

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

Dlaczego jest to takie rozsądne, czyste i proste? Tj. Proszę wyjaśnić różnice w stosunku do innych odpowiedzi, np. Konieczność __iter__i __len__.
Karl Richter

1
@KarlRichter, nigdy nie powiedziałem, że to rozsądne, po prostu dość czyste. ;)
Mike Graham

@KarlRichter, definiuję __iter__i __len__ponieważ muszę, ponieważ wyprowadzam collections.Mapping; sposób użycia collections.Mappingjest dość dobrze omówiony w dokumentacji modułu kolekcji. Inni ludzie nie czują takiej potrzeby, odkąd się wywodzą dict. Wyprowadzanie dictz innego powodu niż zdefiniowanie __missing__jest złym pomysłem. Specyfikacja dict nie mówi, jak działa dict w takim przypadku, aw rzeczywistości skończy się to na tonach metod niewirtualnych, które są mniej przydatne w ogóle, aw tym konkretnym przypadku będą miały metody szczątkowe o nieistotnym zachowaniu.
Mike Graham

7

Wciąż wracam do tego tematu ... Oto kolejna odmiana. Nie radzę sobie z dictdodawaniem __hash__metody podklasy ; Praktycznie nie ma ucieczki od problemu, że dyktanda są zmienne, a ufanie, że się nie zmienią, wydaje się słabym pomysłem. Zamiast tego przyjrzałem się budowaniu mapowania opartego na typie wbudowanym, który sam w sobie jest niezmienny. chociaż tuplejest to oczywisty wybór, dostęp do wartości w nim wymaga sortowania i połowy; nie stanowi problemu, ale wydaje się, że nie wykorzystuje dużej mocy typu, na którym jest zbudowany.

A co, jeśli zablokujesz klucz, wartościowe pary w a frozenset? Czego to wymagałoby, jak by to działało?

Część 1, potrzebujesz sposobu na zakodowanie pozycji w taki sposób, aby zestaw zamrożony traktował je głównie za pomocą kluczy; Zrobię do tego małą podklasę.

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

Już samo to stawia Cię w plucie niezmiennego mapowania:

>>> 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! Niestety, gdy używasz operatorów zbiorów, a elementy są równe, ale nie są tym samym obiektem; która kończy się w wartości zwracanej jest niezdefiniowana , będziemy musieli przejść przez kilka bezwładności.

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

Jednak wyszukiwanie wartości w ten sposób jest uciążliwe, a co gorsza, tworzy wiele zestawów pośrednich; to nie wystarczy! Aby obejść ten problem, stworzymy „fałszywą” parę klucz-wartość:

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

Co powoduje mniej problematyczne:

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

To cała głęboka magia; reszta to pakowanie wszystkiego w coś, co ma interfejs podobny do dyktowania. Ponieważ tworzymy podklasy z frozenset, która ma zupełnie inny interfejs, istnieje wiele metod; otrzymujemy niewielką pomoc collections.Mapping, ale większość pracy polega na zastępowaniu frozensetmetod dla wersji, które działają jak dyktowane, zamiast tego:

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)

co ostatecznie odpowiada na moje własne pytanie:

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

5

Zaakceptowana odpowiedź @Unknown, a także odpowiedź @AlexMartelli działają doskonale, ale tylko pod następującymi ograniczeniami:

  1. Wartości słownika muszą być hashowane. Na przykład hash(hashabledict({'a':[1,2]}))podniesie TypeError.
  2. Klucze muszą obsługiwać operację porównania. Na przykład hash(hashabledict({'a':'a', 1:1}))podniesie TypeError.
  3. Operator porównania na klawiszach narzuca całkowite uporządkowanie. Na przykład, jeśli dwa klucze w słowniku to frozenset((1,2,3))i frozenset((4,5,6)), porównują nierówne w obu kierunkach. Dlatego sortowanie pozycji słownika za pomocą takich kluczy może skutkować arbitralną kolejnością, a zatem narusza zasadę, że równe obiekty muszą mieć tę samą wartość skrótu.

Znacznie szybsza odpowiedź @ObenSonne znosi ograniczenia 2 i 3, ale nadal jest ograniczona przez ograniczenie 1 (wartości muszą być hashowane).

Szybsza, ale odpowiedź @RaymondHettinger znosi wszystkie 3 ograniczenia, ponieważ nie jest uwzględniana .values()w obliczaniu skrótu. Jednak jego działanie jest dobre tylko wtedy, gdy:

  1. Większość (nierównych) słowników, które należy zaszyfrować, nie jest identyczna .keys().

Jeśli ten warunek nie zostanie spełniony, funkcja skrótu będzie nadal ważna, ale może powodować zbyt wiele kolizji. Na przykład w skrajnym przypadku, gdy wszystkie słowniki są generowane z szablonu strony internetowej (nazwy pól jako klucze, dane wejściowe użytkownika jako wartości), klucze będą zawsze takie same, a funkcja skrótu zwróci tę samą wartość dla wszystkich danych wejściowych . W rezultacie tablica hashy, która opiera się na takiej funkcji skrótu, stanie się tak wolna jak lista podczas pobierania elementu ( O(N)zamiast O(1)).

Myślę, że poniższe rozwiązanie będzie działać całkiem dobrze, nawet jeśli wszystkie 4 ograniczenia, które wymieniłem powyżej, zostaną naruszone. Ma dodatkową zaletę, że może haszować nie tylko słowniki, ale także dowolne kontenery, nawet jeśli mają one zagnieżdżone kontenery zmienne.

Byłbym bardzo wdzięczny za wszelkie opinie na ten temat, ponieważ do tej pory testowałem to tylko lekko.

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

2

Możesz również chcieć dodać te dwie metody, aby protokół trawienia v2 działał z instancjami hashdict. W przeciwnym razie cPickle spróbuje użyć hashdict .____ setitem____, co spowoduje błąd TypeError. Co ciekawe, z pozostałymi dwoma wersjami protokołu Twój kod działa dobrze.

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

-2

Jeśli nie umieścisz liczb w słowniku i nigdy nie stracisz zmiennych zawierających Twoje słowniki, możesz to zrobić:

cache[id(rule)] = "whatever"

ponieważ id () jest unikalne dla każdego słownika

EDYTOWAĆ:

Przepraszam, tak w takim razie to, co powiedzieliby inni, byłoby lepsze. Myślę, że możesz również serializować swoje słowniki jako ciąg znaków, na przykład

cache[ 'foo:bar' ] = 'baz'

Jeśli jednak potrzebujesz odzyskać słowniki z kluczy, musisz zrobić coś brzydszego

cache[ 'foo:bar' ] = ( {'foo':'bar'}, 'baz' )

Myślę, że zaletą tego jest to, że nie musiałbyś pisać tak dużo kodu.


Hmmm, nie; to nie jest to, czego szukam cache[id({'foo':'bar'})] = 'baz'; id({'foo':'bar'}) not in cache:, Możliwość dynamicznego tworzenia kluczy jest ważna, gdy chcę używać dykt jako kluczy w pierwszej kolejności.
SingleNegationElimination

1
Serializacja dykt może być w porządku, czy masz zalecenia dotyczące sposobu ich serializacji? tego właśnie szukam.
SingleNegationElimination
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.