Python hashable dicts

94

Sebagai latihan, dan sebagian besar untuk hiburan saya sendiri, saya menerapkan pengurai paket mundur. Inspirasi untuk ini adalah saya ingin memiliki gagasan yang lebih baik tentang bagaimana makro higenis akan bekerja dalam bahasa seperti algol (seperti yang diterapkan pada dialek cadel bebas sintaks yang biasanya Anda temukan di dalamnya). Karena itu, masukan yang berbeda melalui masukan mungkin melihat tata bahasa yang berbeda, sehingga hasil parse yang disimpan dalam cache tidak valid, kecuali saya juga menyimpan versi tata bahasa saat ini bersama dengan hasil parse yang disimpan dalam cache. ( EDIT : konsekuensi dari penggunaan koleksi nilai kunci ini adalah bahwa koleksi tersebut harus tidak dapat diubah, tetapi saya tidak bermaksud untuk mengekspos antarmuka untuk memungkinkannya diubah, jadi koleksi yang dapat diubah atau tidak dapat diubah baik-baik saja)

Masalahnya adalah bahwa dicts python tidak dapat muncul sebagai kunci untuk dicts lain. Bahkan menggunakan tupel (seperti yang akan saya lakukan) tidak membantu.

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

Saya kira itu harus menjadi tupel sampai ke bawah. Sekarang pustaka standar python menyediakan kira-kira apa yang saya perlukan, collections.namedtuplememiliki sintaks yang sangat berbeda, tetapi dapat digunakan sebagai kunci. melanjutkan dari sesi di atas:

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

Baik. Tetapi saya harus membuat kelas untuk setiap kemungkinan kombinasi kunci dalam aturan yang ingin saya gunakan, yang tidak terlalu buruk, karena setiap aturan parse tahu persis parameter apa yang digunakannya, sehingga kelas tersebut dapat didefinisikan pada saat yang sama sebagai fungsi yang mengurai aturan.

Sunting: Masalah tambahan dengan namedtuples adalah bahwa mereka ketat posisional. Dua tupel yang terlihat berbeda sebenarnya bisa sama:

>>> 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: Bagaimana cara mendapatkan dicts yang dapat digunakan sebagai kunci untuk dicts lain ?

Setelah sedikit meretas jawabannya, inilah solusi yang lebih lengkap yang saya gunakan. Perhatikan bahwa ini melakukan sedikit pekerjaan ekstra untuk membuat penis yang dihasilkan tidak dapat diubah untuk tujuan praktis. Tentu saja masih cukup mudah untuk meretasnya dengan menelepon dict.__setitem__(instance, key, value)tetapi kita semua orang dewasa di sini.

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()
SingleNegationElimination
sumber
The hashdictharus berubah, setidaknya setelah Anda mulai hashing, jadi mengapa tidak cache keydan hashnilai-nilai sebagai atribut dari hashdictobjek? Saya memodifikasi __key()dan __hash__(), dan menguji untuk memastikan bahwa ini jauh lebih cepat. SO tidak mengizinkan kode berformat dalam komentar, jadi saya akan menautkannya di sini: sam.aiki.info/hashdict.py
Sam Watkins

Jawaban:

71

Berikut cara mudah membuat kamus hashable. Ingatlah untuk tidak mengubahnya setelah disematkan di kamus lain karena alasan yang jelas.

class hashabledict(dict):
    def __hash__(self):
        return hash(tuple(sorted(self.items())))
Tidak diketahui
sumber
7
Ini tidak secara tajam memastikan konsistensi eq dan hash sementara jawaban saya sebelumnya dilakukan melalui penggunaan metode __key (dalam praktiknya, pendekatan mana pun harus berfungsi, meskipun yang ini mungkin diperlambat dengan membuat daftar itermediate yang tidak diperlukan - dapat diperbaiki oleh s / items / iteritems / - dengan asumsi Python 2. * seperti yang tidak Anda katakan ;-).
Alex Martelli
5
Mungkin akan lebih baik hanya menggunakan frozenset daripada tupel dengan penyortiran. Ini tidak hanya akan lebih cepat, tetapi Anda tidak dapat berasumsi bahwa kunci kamus dapat dibandingkan.
asmeurer
1
Sepertinya harus ada cara untuk menghindari fungsi hash di O(n*log(n))situlah njumlah dictentri. Adakah yang tahu jika frozensetfungsi hash Python berjalan dalam waktu linier?
Tom Karzes
2
@HelloGoodbye Dikt juga dapat dibuat seperti ini dict(key1=value1, key2=value2,...)atau ini dict([(key1, value1), (key2, value2),...)]). Hal yang sama berlaku untuk yang satu ini. Kreasi yang Anda posting disebut literal
smido
2
@smido: Terima kasih. Saya juga menemukan bahwa Anda hanya dapat melemparkan secara literal, yaitu hashabledict({key_a: val_a, key_b: val_b, ...}).
HelloGoodbye
62

Hashable harus tidak dapat diubah - tidak memaksakan ini tetapi PERCAYA Anda untuk tidak mengubah dikt setelah digunakan pertama kali sebagai kunci, pendekatan berikut akan berfungsi:

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

Jika Anda HARUS mengubah penis Anda dan MASIH ingin menggunakannya sebagai kunci, kompleksitas meledak ratusan kali lipat - bukan untuk mengatakan itu tidak dapat dilakukan, tetapi saya akan menunggu sampai indikasi yang SANGAT spesifik sebelum saya masuk ke dalam rawa yang luar biasa ITU! -)

Alex Martelli
sumber
Saya pasti tidak ingin mengubah penis setelah mereka siap. Itu akan membuat algoritma packrad lainnya berantakan.
SingleNegationElimination
Kemudian subclass yang saya sarankan akan berfungsi - perhatikan bagaimana subclass ini melewati masalah "posisi" ( sebelum Anda mengedit pertanyaan Anda untuk menunjukkannya ;-) dengan sortedin __key ;-).
Alex Martelli
Perilaku yang bergantung pada posisi dari putri bernama mengejutkanku. Saya telah bermain-main dengannya, berpikir itu mungkin masih cara yang lebih mudah untuk menyelesaikan masalah, tetapi itu cukup memupuskan semua harapan saya (dan akan membutuhkan rollback :()
SingleNegationElimination
Katakanlah saya memiliki dict dan saya ingin memasukkannya ke hashabledict. Bagaimana saya melakukannya?
jononomo
32

Semua yang diperlukan untuk membuat kamus dapat digunakan untuk tujuan Anda adalah menambahkan metode __hash__:

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

Perhatikan, konversi frozenset akan bekerja untuk semua kamus (yaitu tidak memerlukan kunci untuk diurutkan). Demikian juga, tidak ada batasan pada nilai kamus.

Jika ada banyak kamus dengan kunci yang identik tetapi dengan nilai yang berbeda, hash perlu mempertimbangkan nilai-nilainya. Cara tercepat untuk melakukannya adalah:

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

Ini lebih cepat dari frozenset(self.iteritems())dua alasan. Pertama, frozenset(self)langkah tersebut menggunakan kembali nilai hash yang disimpan dalam kamus, menyimpan panggilan yang tidak perlu ke hash(key). Kedua, menggunakan itervalues akan mengakses nilai secara langsung dan menghindari banyak panggilan pengalokasi memori yang digunakan oleh item untuk membentuk banyak tupel kunci / nilai baru dalam memori setiap kali Anda melakukan pencarian.

Raymond Hettinger
sumber
@RaymondHettinger Koreksi saya jika saya salah, tetapi saya pikir dictitu sendiri tidak menyimpan nilai hash kuncinya - meskipun kelas individu (seperti str) mungkin dan memang memilih untuk menyimpan hash mereka. Setidaknya ketika saya membuat dictdengan instance kelas khusus saya yang digunakan sebagai kunci, __hash__metode mereka dipanggil pada setiap operasi akses (python 3.4). Apakah saya benar atau tidak, saya tidak yakin bagaimana hash(frozenset(self))dapat menggunakan kembali nilai hash yang telah dihitung sebelumnya, kecuali mereka di-cache di dalam kunci itu sendiri (dalam hal ini, hash(frozenset(self.items())gunakan kembali juga).
maks
Mengenai poin kedua Anda tentang pembuatan tupel (kunci / nilai), saya pikir .items () metode mengembalikan tampilan daripada daftar tupel, dan bahwa pembuatan tampilan itu tidak melibatkan penyalinan kunci dan nilai yang mendasarinya. (Python 3.4 lagi.) Yang mengatakan, saya melihat keuntungan dari hashing hanya kunci jika sebagian besar input memiliki kunci yang berbeda - karena (1) itu cukup mahal untuk nilai-nilai hash, dan (2) itu cukup membatasi untuk mengharuskan nilai-nilai tersebut dapat di-hash
maks
6
Ini juga memiliki kemungkinan untuk membuat hash yang sama untuk dua kamus yang berbeda. Pertimbangkan {'one': 1, 'two': 2}dan{'one': 2, 'two': 1}
AgDude
Mike Graham dalam komentarnya menyatakan bahwa menurunkan dikt untuk alasan lain selain mendefinisikan __missing__adalah ide yang buruk. Bagaimana menurut anda?
Piotr Dobrogost
1
Subclassing dari dict telah didefinisikan dengan baik sejak Python 2.2. Lihat collections.OrderedDict dan collections.Counter untuk contoh dari pustaka standar Python. Komentar lain didasarkan pada keyakinan tidak berdasar bahwa hanya subclass MutableMapping yang didefinisikan dengan baik.
Raymond Hettinger
23

Jawaban yang diberikan baik-baik saja, tetapi dapat ditingkatkan dengan menggunakan frozenset(...)alih-alih tuple(sorted(...))menghasilkan hash:

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

Keunggulan performa bergantung pada konten kamus, tetapi dalam kebanyakan kasus yang telah saya uji, melakukan hashing frozensetsetidaknya 2 kali lebih cepat (terutama karena tidak perlu diurutkan).

Oben Sonne
sumber
1
Catatan, tidak perlu menyertakan kunci dan dan nilai. Solusi ini akan lebih cepat sebagai: hash(frozenset(d)).
Raymond Hettinger
10
@RaymondHettinger: hash(frozenset(d))menghasilkan hash yang identik untuk 2 kode dengan kunci serupa tetapi nilai berbeda!
Oben Sonne
4
Itu tidak masalah. Ini adalah tugas __eq__ untuk membedakan antara penis dengan nilai yang berbeda. Pekerjaan __hash__ hanyalah untuk mengurangi ruang pencarian.
Raymond Hettinger
5
Itu benar untuk konsep teoritis hash dan pemetaan tetapi tidak praktis untuk cache dengan kamus sebagai pencarian - tidak jarang kamus dengan kunci yang mirip tetapi nilai berbeda diteruskan ke fungsi mem-cache. Dalam hal ini cache secara praktis berubah menjadi daftar dan bukan pemetaan jika hanya kunci yang digunakan untuk membuat hash.
Oben Sonne
3
Dalam kasus khusus dicts dengan kunci indentikal dan nilai yang berbeda, Anda akan lebih baik hanya menyimpan berdasarkan hash frozenset(d.itervalues()). Dalam kasus di mana dicts memiliki kunci yang berbeda, frozenset(d)adalah jauh lebih cepat dan tidak memaksakan pembatasan pada hashability kunci. Terakhir, ingat bahwa metode dict .__ eq__ akan memeriksa pasangan kunci / nilai yang sama jauh lebih cepat sehingga apa pun dapat menghitung hash untuk semua tupel pasangan kunci / nilai. Menggunakan tupel kunci / nilai juga bermasalah karena membuang hash yang disimpan untuk semua kunci (itulah sebabnya frozenset(d)sangat cepat).
Raymond Hettinger
11

Penerapan yang cukup bersih dan langsung adalah

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())))
Mike Graham
sumber
Mengapa ini sangat masuk akal, bersih dan lugas? Yaitu tolong jelaskan perbedaan jawaban lain, misalnya perlunya __iter__dan __len__.
Karl Richter
1
@KarlRich, saya tidak pernah mengatakan itu masuk akal, hanya cukup bersih. ;)
Mike Graham
@KarlRichter, saya mendefinisikan __iter__dan __len__karena saya harus, karena saya turun collections.Mapping; cara menggunakan collections.Mappingdibahas dengan cukup baik dalam dokumentasi modul koleksi. Orang lain tidak merasa perlu sejak mereka turun dict. Turun dictuntuk alasan lain selain mendefinisikan __missing__adalah ide yang buruk. Spec dict tidak menjelaskan bagaimana dict bekerja dalam kasus seperti itu, dan pada kenyataannya ini akan menghasilkan banyak sekali metode non-virtual yang kurang berguna secara umum dan dalam kasus khusus ini akan memiliki metode vestigial dengan perilaku yang tidak relevan.
Mike Graham
7

Saya terus kembali ke topik ini ... Ini variasi lainnya. Saya tidak nyaman dengan subclassing dictuntuk menambahkan __hash__metode; Hampir tidak ada jalan keluar dari masalah bahwa dict dapat berubah, dan percaya bahwa mereka tidak akan berubah sepertinya ide yang lemah. Jadi saya malah melihat membangun pemetaan berdasarkan jenis bawaan yang itu sendiri tidak dapat diubah. meskipun tuplemerupakan pilihan yang jelas, mengakses nilai-nilai di dalamnya menyiratkan semacam dan pembagian; bukan masalah, tetapi tampaknya tidak banyak memanfaatkan kekuatan dari jenisnya.

Bagaimana jika Anda memasukkan kunci, nilai berpasangan menjadi a frozenset? Apa yang dibutuhkan, bagaimana cara kerjanya?

Bagian 1, Anda memerlukan cara untuk menyandikan 'item' sedemikian rupa sehingga frozenset akan memperlakukannya terutama dengan kunci mereka; Saya akan membuat subclass kecil untuk itu.

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

Itu saja menempatkan Anda dalam jarak meludah dari pemetaan yang tidak dapat diubah:

>>> 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! Sayangnya, saat Anda menggunakan operator set dan elemennya sama tetapi bukan objek yang sama; yang mana yang berakhir dengan nilai kembali tidak ditentukan , kita harus melalui beberapa putaran lagi.

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

Namun, mencari nilai dengan cara ini tidak praktis, dan lebih buruk lagi, menciptakan banyak set perantara; itu tidak akan berhasil! Kami akan membuat pasangan nilai kunci 'palsu' untuk menyiasatinya:

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

Yang menghasilkan tidak terlalu bermasalah:

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

Itu semua keajaiban yang dalam; sisanya membungkus semuanya menjadi sesuatu yang memiliki antarmuka seperti dikt. Karena kita subclass dari frozenset, yang memiliki antarmuka yang sangat berbeda, ada cukup banyak metode; kami mendapat sedikit bantuan collections.Mapping, tetapi sebagian besar pekerjaan itu menimpa frozensetmetode untuk versi yang bekerja seperti dicts, sebagai gantinya:

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)

yang pada akhirnya menjawab pertanyaan saya sendiri:

>>> 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
SingleNegationElimination
sumber
5

Jawaban yang diterima oleh @Unknown, serta jawaban dari @AlexMartelli berfungsi dengan baik, tetapi hanya di bawah batasan berikut:

  1. Nilai kamus harus dapat dicirikan. Misalnya, hash(hashabledict({'a':[1,2]}))akan menaikkan TypeError.
  2. Kunci harus mendukung operasi perbandingan. Misalnya, hash(hashabledict({'a':'a', 1:1}))akan menaikkan TypeError.
  3. Operator perbandingan pada kunci memberlakukan pengurutan total. Misalnya, jika dua kunci dalam kamus adalah frozenset((1,2,3))dan frozenset((4,5,6)), mereka membandingkan tidak sama di kedua arah. Oleh karena itu, mengurutkan item kamus dengan kunci tersebut dapat menghasilkan urutan arbitrer, dan oleh karena itu akan melanggar aturan bahwa objek yang sama harus memiliki nilai hash yang sama.

Jawaban yang jauh lebih cepat oleh @ObenSonne menghilangkan batasan 2 dan 3, tetapi masih terikat oleh batasan 1 (nilai harus dapat di-hash).

Jawaban yang lebih cepat namun oleh @RaymondHettinger menghilangkan semua 3 kendala karena tidak termasuk .values()dalam perhitungan hash. Namun, kinerjanya hanya baik jika:

  1. Sebagian besar kamus (tidak setara) yang perlu di-hash tidak identik .keys().

Jika kondisi ini tidak terpenuhi, fungsi hash akan tetap valid, tetapi dapat menyebabkan terlalu banyak benturan. Misalnya, dalam kasus ekstrem di mana semua kamus dibuat dari templat situs web (nama bidang sebagai kunci, masukan pengguna sebagai nilai), kunci akan selalu sama, dan fungsi hash akan mengembalikan nilai yang sama untuk semua masukan . Akibatnya, hashtable yang bergantung pada fungsi hash seperti itu akan menjadi lambat seperti daftar saat mengambil item ( O(N)bukan O(1)).

Saya pikir solusi berikut akan bekerja dengan cukup baik bahkan jika semua 4 kendala yang saya sebutkan di atas dilanggar. Ini memiliki keuntungan tambahan karena tidak hanya dapat meng-hash kamus, tetapi juga penampung apa pun, bahkan jika penampung tersebut memiliki penampung yang dapat diubah bersarang.

Saya sangat menghargai umpan balik apa pun tentang ini, karena saya hanya menguji ini dengan ringan sejauh ini.

# 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))
maks
sumber
2

Anda mungkin juga ingin menambahkan kedua metode ini agar protokol pengawetan v2 berfungsi dengan instance hashdict. Jika tidak, cPickle akan mencoba menggunakan hashdict .____ setitem____ yang menghasilkan TypeError. Menariknya, dengan dua versi protokol lainnya, kode Anda berfungsi dengan baik.

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

Jika Anda tidak memasukkan angka ke dalam kamus dan Anda tidak pernah kehilangan variabel yang berisi kamus Anda, Anda dapat melakukan ini:

cache[id(rule)] = "whatever"

karena id () unik untuk setiap kamus

EDIT:

Oh maaf, ya kalau begitu apa yang dikatakan orang lain akan lebih baik. Saya pikir Anda juga bisa membuat serial kamus Anda sebagai string, seperti

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

Jika Anda perlu memulihkan kamus Anda dari kunci, maka Anda harus melakukan sesuatu yang lebih buruk seperti

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

Saya kira keuntungan dari ini adalah Anda tidak perlu menulis banyak kode.

Deepthroat
sumber
Hmmm, tidak; ini bukan yang saya Cari cache[id({'foo':'bar'})] = 'baz'; id({'foo':'bar'}) not in cache:, Mampu membuat kunci secara dinamis sangat penting ketika saya ingin menggunakan dicts sebagai kunci di tempat pertama.
SingleNegationElimination
1
Serialisasi dicts mungkin baik-baik saja, apakah Anda memiliki rekomendasi tentang cara menserialisasinya? itulah yang saya cari.
SingleNegationElimination