En tant qu'exercice, et surtout pour mon propre amusement, j'implémente un analyseur de paquets de retour en arrière. L'inspiration pour cela est que j'aimerais avoir une meilleure idée de la façon dont les macros hygiéniques fonctionneraient dans un langage de type algol (comme associé aux dialectes lisp sans syntaxe dans lesquels vous les trouvez normalement). Pour cette raison, différentes passes dans l'entrée peuvent voir des grammaires différentes, donc les résultats d'analyse mis en cache ne sont pas valides, sauf si je stocke également la version actuelle de la grammaire avec les résultats d'analyse mis en cache. ( EDIT : une conséquence de cette utilisation des collections clé-valeur est qu'elles devraient être immuables, mais je n'ai pas l'intention d'exposer l'interface pour leur permettre d'être modifiées, donc les collections mutables ou immuables conviennent)
Le problème est que les dictionnaires python ne peuvent pas apparaître comme des clés d'autres dictionnaires. Même utiliser un tuple (comme je le ferais de toute façon) n'aide pas.
>>> cache = {}
>>> rule = {"foo":"bar"}
>>> cache[(rule, "baz")] = "quux"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'dict'
>>>
Je suppose que ce doit être des tuples tout en bas. Maintenant, la bibliothèque standard python fournit approximativement ce dont j'aurais besoin, collections.namedtuple
a une syntaxe très différente, mais peut être utilisée comme clé. suite de la session ci-dessus:
>>> from collections import namedtuple
>>> Rule = namedtuple("Rule",rule.keys())
>>> cache[(Rule(**rule), "baz")] = "quux"
>>> cache
{(Rule(foo='bar'), 'baz'): 'quux'}
D'accord. Mais je dois créer une classe pour chaque combinaison possible de clés dans la règle que je voudrais utiliser, ce qui n'est pas si mal, car chaque règle d'analyse sait exactement quels paramètres elle utilise, afin que cette classe puisse être définie en même temps comme fonction qui analyse la règle.
Edit: Un problème supplémentaire avec les namedtuple
s est qu'ils sont strictement positionnels. Deux tuples qui semblent devoir être différents peuvent en fait être identiques:
>>> 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: Comment obtenir des dict
s qui peuvent être utilisés comme clés pour d'autres dict
s?
Après avoir piraté un peu les réponses, voici la solution la plus complète que j'utilise. Notez que cela fait un peu plus de travail pour rendre les dictats résultants vaguement immuables à des fins pratiques. Bien sûr, il est toujours assez facile de pirater en appelant, dict.__setitem__(instance, key, value)
mais nous sommes tous des adultes ici.
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
doit être immuable, au moins après avoir commencé à le hacher, alors pourquoi ne pas mettre en cache les valeurskey
et enhash
tant qu'attributs duhashdict
objet? J'ai modifié__key()
et__hash__()
, et testé pour confirmer qu'il est beaucoup plus rapide. SO n'autorise pas le code formaté dans les commentaires, je vais donc le lier ici: sam.aiki.info/hashdict.pyRéponses:
Voici le moyen le plus simple de créer un dictionnaire hachable. N'oubliez pas de ne pas les muter après les avoir intégrés dans un autre dictionnaire pour des raisons évidentes.
la source
O(n*log(n))
où sen
trouve le nombre d'dict
entrées. Est-ce que quelqu'un sait si lafrozenset
fonction de hachage de Python s'exécute en temps linéaire?dict(key1=value1, key2=value2,...)
ou cecidict([(key1, value1), (key2, value2),...)])
. La même chose s'applique à celui-ci. La création que vous avez postée s'appelle un littéralhashabledict({key_a: val_a, key_b: val_b, ...})
.Les hashables doivent être immuables - ne pas appliquer cela mais VOUS CONFIANT de ne pas muter un dict après sa première utilisation comme clé, l'approche suivante fonctionnerait:
Si vous avez besoin de muter vos dictés et que vous voulez TOUJOURS les utiliser comme clés, la complexité explose au centuple - pour ne pas dire que cela ne peut pas être fait, mais j'attendrai une indication TRÈS spécifique avant de me lancer dans CET incroyable marais! -)
la source
sorted
in __key ;-).Tout ce qui est nécessaire pour rendre les dictionnaires utilisables pour votre objectif est d'ajouter une méthode __hash__:
Noter la conversion de frozenset fonctionnera pour tous les dictionnaires (c'est-à-dire qu'elle ne nécessite pas que les clés soient triables). De même, il n'y a aucune restriction sur les valeurs du dictionnaire.
S'il existe de nombreux dictionnaires avec des clés identiques mais avec des valeurs distinctes, il est nécessaire que le hachage prenne en compte les valeurs. Le moyen le plus rapide d'y parvenir est:
C'est plus rapide que
frozenset(self.iteritems())
pour deux raisons. Tout d'abord, l'frozenset(self)
étape réutilise les valeurs de hachage stockées dans le dictionnaire, en économisant les appels inutiles àhash(key)
. Deuxièmement, l'utilisation d' itervalues accédera directement aux valeurs et évitera les nombreux appels d'allocateur de mémoire utilisant by items pour former de nouveaux nombreux tuples clé / valeur en mémoire chaque fois que vous effectuez une recherche.la source
dict
lui-même ne mettait pas en cache les valeurs de hachage de ses clés - bien que des classes individuelles (commestr
) puissent et choisissent de mettre en cache leurs hachages. Au moins lorsque j'ai créé undict
avec mes instances de classe personnalisées utilisées comme clés, leurs__hash__
méthodes étaient appelées à chaque opération d'accès (python 3.4). Que j'aie raison ou non, je ne sais pas commenthash(frozenset(self))
réutiliser les valeurs de hachage précalculées, à moins qu'elles ne soient mises en cache à l'intérieur des clés elles-mêmes (dans ce cas, leshash(frozenset(self.items())
réutilise également).{'one': 1, 'two': 2}
et{'one': 2, 'two': 1}
__missing__
est une mauvaise idée. Qu'est-ce que tu penses?Les réponses données sont correctes, mais elles pourraient être améliorées en utilisant
frozenset(...)
au lieu detuple(sorted(...))
pour générer le hachage:L'avantage en termes de performances dépend du contenu du dictionnaire, mais dans la plupart des cas que j'ai testés, le hachage avec
frozenset
est au moins 2 fois plus rapide (principalement parce qu'il n'a pas besoin de trier).la source
hash(frozenset(d))
.hash(frozenset(d))
donne des hachages identiques pour 2 dictionnaires avec des clés similaires mais des valeurs différentes!frozenset(d.itervalues())
. Dans les cas où les dictionnaires ont des clés distinctes,frozenset(d)
c'est beaucoup plus rapide et n'impose aucune restriction sur la capacité de hachage des clés. Enfin, rappelez-vous que la méthode dict .__ eq__ vérifiera les paires clé / valeur égales beaucoup plus rapidement que n'importe quoi peut calculer le hachage pour tous les tuples de paire clé / valeur. L'utilisation de tuples clé / valeur est également problématique car elle jette les hachages stockés pour toutes les clés (c'est pourquoifrozenset(d)
c'est si rapide).Une implémentation raisonnablement propre et simple est
la source
__iter__
et__len__
.__iter__
et__len__
parce que je dois, puisque je dérivecollections.Mapping
; son utilisationcollections.Mapping
est assez bien décrite dans la documentation du module collections. D'autres personnes n'en ressentent pas le besoin puisqu'elles dériventdict
. Dériverdict
pour toute autre raison que pour définir__missing__
est une mauvaise idée. La spécification de dict ne dit pas comment dict fonctionne dans un tel cas, et en réalité, cela finira par avoir des tonnes de méthodes non virtuelles qui sont moins utiles en général et dans ce cas particulier, des méthodes résiduelles avec un comportement non pertinent.Je reviens sans cesse sur ce sujet ... Voici une autre variante. Je ne suis pas à l'aise avec le sous-classement
dict
pour ajouter une__hash__
méthode; Il n'y a pratiquement pas d'échappatoire au problème que les dict sont mutables, et espérer qu'ils ne changeront pas semble être une idée faible. J'ai donc plutôt cherché à créer un mappage basé sur un type intégré qui est lui-même immuable. bien que cetuple
soit un choix évident, y accéder aux valeurs implique un tri et une bissectrice; ce n'est pas un problème, mais il ne semble pas exploiter une grande partie de la puissance du type sur lequel il est construit.Que faire si vous bloquez la clé, la valeur des paires dans un
frozenset
? Qu'est-ce que cela exigerait, comment cela fonctionnerait-il?Dans la première partie, vous avez besoin d'un moyen d'encoder les «éléments» de telle sorte qu'un frozenset les traite principalement par leurs clés; Je vais faire une petite sous-classe pour cela.
Cela seul vous met à l'écart d'une cartographie immuable:
Oh! Malheureusement, lorsque vous utilisez les opérateurs set et les éléments sont égaux mais pas le même objet; celui qui se termine dans la valeur de retour n'est pas défini , nous devrons passer par d'autres girations.
Cependant, rechercher les valeurs de cette manière est fastidieux et, pire, crée de nombreux ensembles intermédiaires; ça ne va pas! Nous allons créer une `` fausse '' paire valeur / clé pour la contourner:
Ce qui se traduit par le moins problématique:
C'est toute la magie profonde; le reste englobe tout cela dans quelque chose qui a une interface comme un dict. Puisque nous sous-classons de
frozenset
, qui a une interface très différente, il y a pas mal de méthodes; nous obtenons un peu d'aidecollections.Mapping
, mais la plupart du travail remplace lesfrozenset
méthodes pour les versions qui fonctionnent comme des dictionnaires, à la place:qui, en fin de compte, répond à ma propre question:
la source
La réponse acceptée par @Unknown, ainsi que la réponse de @AlexMartelli fonctionnent parfaitement bien, mais uniquement sous les contraintes suivantes:
hash(hashabledict({'a':[1,2]}))
augmenteraTypeError
.hash(hashabledict({'a':'a', 1:1}))
augmenteraTypeError
.frozenset((1,2,3))
etfrozenset((4,5,6))
, elles se comparent de manière inégale dans les deux sens. Par conséquent, le tri des éléments d'un dictionnaire avec de telles clés peut entraîner un ordre arbitraire et violera par conséquent la règle selon laquelle les objets égaux doivent avoir la même valeur de hachage.La réponse beaucoup plus rapide de @ObenSonne lève les contraintes 2 et 3, mais est toujours liée par la contrainte 1 (les valeurs doivent être hachables).
La réponse plus rapide encore de @RaymondHettinger lève les 3 contraintes car elle n'inclut pas
.values()
dans le calcul de hachage. Cependant, ses performances ne sont bonnes que si:.keys()
.Si cette condition n'est pas satisfaite, la fonction de hachage sera toujours valide, mais peut provoquer trop de collisions. Par exemple, dans le cas extrême où tous les dictionnaires sont générés à partir d'un modèle de site Web (noms de champs en tant que clés, entrées de l'utilisateur en tant que valeurs), les clés seront toujours les mêmes et la fonction de hachage renverra la même valeur pour toutes les entrées . En conséquence, une table de hachage qui repose sur une telle fonction de hachage deviendra aussi lente qu'une liste lors de la récupération d'un élément (
O(N)
au lieu deO(1)
).Je pense que la solution suivante fonctionnera raisonnablement bien même si les 4 contraintes énumérées ci-dessus sont violées. Il présente un avantage supplémentaire: il peut hacher non seulement des dictionnaires, mais tous les conteneurs, même s'ils ont des conteneurs mutables imbriqués.
J'apprécierais beaucoup vos commentaires à ce sujet, car je n'ai testé cela que légèrement jusqu'à présent.
la source
Vous pouvez également ajouter ces deux méthodes pour que le protocole de décapage v2 fonctionne avec les instances de hashdict. Sinon, cPickle essaiera d'utiliser hashdict .____ setitem____ résultant en une TypeError. Fait intéressant, avec les deux autres versions du protocole, votre code fonctionne très bien.
la source
Si vous ne mettez pas de nombres dans le dictionnaire et que vous ne perdez jamais les variables contenant vos dictionnaires, vous pouvez faire ceci:
cache[id(rule)] = "whatever"
puisque id () est unique pour chaque dictionnaire
ÉDITER:
Oh désolé, ouais dans ce cas ce que les autres gars ont dit serait mieux. Je pense que vous pouvez également sérialiser vos dictionnaires sous forme de chaîne, comme
cache[ 'foo:bar' ] = 'baz'
Si vous avez besoin de récupérer vos dictionnaires à partir des clés, vous devrez faire quelque chose de plus laid comme
cache[ 'foo:bar' ] = ( {'foo':'bar'}, 'baz' )
J'imagine que l'avantage de ceci est que vous n'auriez pas à écrire autant de code.
la source
cache[id({'foo':'bar'})] = 'baz'; id({'foo':'bar'}) not in cache
être capable de créer dynamiquement des clés est important lorsque je veux utiliser des dictées comme clés en premier lieu.