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.namedtuple
memiliki 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 namedtuple
s 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 dict
s yang dapat digunakan sebagai kunci untuk dict
s 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()
hashdict
harus berubah, setidaknya setelah Anda mulai hashing, jadi mengapa tidak cachekey
danhash
nilai-nilai sebagai atribut darihashdict
objek? 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.pyJawaban:
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())))
sumber
O(n*log(n))
situlahn
jumlahdict
entri. Adakah yang tahu jikafrozenset
fungsi hash Python berjalan dalam waktu linier?dict(key1=value1, key2=value2,...)
atau inidict([(key1, value1), (key2, value2),...)])
. Hal yang sama berlaku untuk yang satu ini. Kreasi yang Anda posting disebut literalhashabledict({key_a: val_a, key_b: val_b, ...})
.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! -)
sumber
sorted
in __key ;-).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 kehash(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.sumber
dict
itu sendiri tidak menyimpan nilai hash kuncinya - meskipun kelas individu (sepertistr
) mungkin dan memang memilih untuk menyimpan hash mereka. Setidaknya ketika saya membuatdict
dengan 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 bagaimanahash(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).{'one': 1, 'two': 2}
dan{'one': 2, 'two': 1}
__missing__
adalah ide yang buruk. Bagaimana menurut anda?Jawaban yang diberikan baik-baik saja, tetapi dapat ditingkatkan dengan menggunakan
frozenset(...)
alih-alihtuple(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
frozenset
setidaknya 2 kali lebih cepat (terutama karena tidak perlu diurutkan).sumber
hash(frozenset(d))
.hash(frozenset(d))
menghasilkan hash yang identik untuk 2 kode dengan kunci serupa tetapi nilai berbeda!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 sebabnyafrozenset(d)
sangat cepat).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())))
sumber
__iter__
dan__len__
.__iter__
dan__len__
karena saya harus, karena saya turuncollections.Mapping
; cara menggunakancollections.Mapping
dibahas dengan cukup baik dalam dokumentasi modul koleksi. Orang lain tidak merasa perlu sejak mereka turundict
. Turundict
untuk 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.Saya terus kembali ke topik ini ... Ini variasi lainnya. Saya tidak nyaman dengan subclassing
dict
untuk 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. meskipuntuple
merupakan 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 bantuancollections.Mapping
, tetapi sebagian besar pekerjaan itu menimpafrozenset
metode 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
sumber
Jawaban yang diterima oleh @Unknown, serta jawaban dari @AlexMartelli berfungsi dengan baik, tetapi hanya di bawah batasan berikut:
hash(hashabledict({'a':[1,2]}))
akan menaikkanTypeError
.hash(hashabledict({'a':'a', 1:1}))
akan menaikkanTypeError
.frozenset((1,2,3))
danfrozenset((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:.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)
bukanO(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))
sumber
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),)
sumber
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.
sumber
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.