web-dev-qa-db-fra.com

Comment limiter la taille d'un dictionnaire?

Je voudrais travailler avec un dict en python, mais limiter le nombre de paires clé/valeur à X. En d'autres termes, si le dict stocke actuellement X paires clé/valeur et que j'effectue une insertion, je voudrais l'une des les paires existantes à supprimer. Ce serait bien s'il s'agissait de la clé insérée/d'accès la moins récente, mais ce n'est pas complètement nécessaire.

Si cela existe dans la bibliothèque standard, veuillez me faire gagner du temps et le signaler!

55
anthony

Python 2.7 et 3.1 ont OrderedDict et il existe des implémentations en Python pur pour les Pythons antérieurs.

from collections import OrderedDict

class LimitedSizeDict(OrderedDict):
    def __init__(self, *args, **kwds):
        self.size_limit = kwds.pop("size_limit", None)
        OrderedDict.__init__(self, *args, **kwds)
        self._check_size_limit()

    def __setitem__(self, key, value):
        OrderedDict.__setitem__(self, key, value)
        self._check_size_limit()

    def _check_size_limit(self):
        if self.size_limit is not None:
            while len(self) > self.size_limit:
                self.popitem(last=False)

Vous devrez également remplacer d'autres méthodes pouvant insérer des éléments, telles que update. L'usage principal de OrderedDict est que vous pouvez contrôler ce qui est facilement sauté, sinon un dict normal fonctionnerait.

41
Roger Pate

cachetools vous fournira une belle implémentation de Mapping Hashes qui fait cela (et cela fonctionne sur python 2 et 3).

Extrait de la documentation:

Aux fins de ce module, un cache est un mappage mutable d'une taille maximale fixe. Lorsque le cache est plein, c'est-à-dire qu'en ajoutant un autre élément, le cache dépasserait sa taille maximale, le cache doit choisir le ou les éléments à rejeter en fonction d'un algorithme de cache approprié.

17
vaab

Voici une solution simple, sans LRU Python 2.6+) (dans les anciens Pythons, vous pourriez faire quelque chose de similaire avec UserDict.DictMixin, mais en 2.6 et mieux ce n'est pas recommandé, et les ABC de collections sont quand même préférables ...):

import collections

class MyDict(collections.MutableMapping):
    def __init__(self, maxlen, *a, **k):
        self.maxlen = maxlen
        self.d = dict(*a, **k)
        while len(self) > maxlen:
            self.popitem()
    def __iter__(self):
        return iter(self.d)
    def __len__(self):
        return len(self.d)
    def __getitem__(self, k):
        return self.d[k]
    def __delitem__(self, k):
        del self.d[k]
    def __setitem__(self, k, v):
        if k not in self and len(self) == self.maxlen:
            self.popitem()
        self.d[k] = v

d = MyDict(5)
for i in range(10):
    d[i] = i
    print(sorted(d))

Comme d'autres réponses l'ont mentionné, vous ne voudrez probablement pas sous-classer dict - la délégation explicite à self.d est malheureusement passe-partout, mais garantie que toutes les autres méthodes sont correctement fournies par collections.MutableMapping.

10
Alex Martelli

Voici un cache LRU simple et efficace écrit avec de la saleté simple Python code qui s'exécute sur n'importe quel python version 1.5.2 ou ultérieure:

class LRU_Cache:

    def __init__(self, original_function, maxsize=1000):
        self.original_function = original_function
        self.maxsize = maxsize
        self.mapping = {}

        PREV, NEXT, KEY, VALUE = 0, 1, 2, 3         # link fields
        self.head = [None, None, None, None]        # oldest
        self.tail = [self.head, None, None, None]   # newest
        self.head[NEXT] = self.tail

    def __call__(self, *key):
        PREV, NEXT = 0, 1
        mapping, head, tail = self.mapping, self.head, self.tail

        link = mapping.get(key, head)
        if link is head:
            value = self.original_function(*key)
            if len(mapping) >= self.maxsize:
                old_prev, old_next, old_key, old_value = head[NEXT]
                head[NEXT] = old_next
                old_next[PREV] = head
                del mapping[old_key]
            last = tail[PREV]
            link = [last, tail, key, value]
            mapping[key] = last[NEXT] = tail[PREV] = link
        else:
            link_prev, link_next, key, value = link
            link_prev[NEXT] = link_next
            link_next[PREV] = link_prev
            last = tail[PREV]
            last[NEXT] = tail[PREV] = link
            link[PREV] = last
            link[NEXT] = tail
        return value

if __== '__main__':
    p = LRU_Cache(pow, maxsize=3)
    for i in [1,2,3,4,5,3,1,5,1,1]:
        print(i, p(i, 2))
8
Raymond Hettinger

Un dict n'a pas ce comportement. Vous pouvez créer votre propre classe qui fait cela, par exemple quelque chose comme

class MaxSizeDict(object):
    def __init__(self, max_size):
        self.max_size = max_size
        self.dict = {}
    def __setitem__(self, key, value):
        if key in self.dict:
            self.dict[key] = value    
            return

        if len(self.dict) >= self.max_size:
      ...

Quelques notes à ce sujet

  • Il serait tentant pour certains de sous-classer dict ici. Vous pouvez techniquement le faire, mais cela est sujet aux bogues car les méthodes ne dépendent pas les unes des autres. Vous pouvez utiliser UserDict.DictMixin pour éviter d'avoir à définir toutes les méthodes. Il y a peu de méthodes que vous pourriez réutiliser si vous sous-classe dict.
  • Un dict ne sait pas quelle est la clé la moins récemment ajoutée, car les dicts ne sont pas ordonnés.
    • 2.7 introduira collections.OrderedDict, mais pour l'instant garder les clés dans l'ordre séparément devrait fonctionner correctement (utilisez un collections.deque comme file d'attente).
    • Si obtenir le plus ancien n'est pas si important, vous pouvez simplement utiliser la méthode popitem pour supprimer un élément arbitraire.
  • J'ai interprété l'aîné comme signifiant la première insertion, approximativement. Il faudrait faire quelque chose d'un peu différent pour éliminer les éléments LRU. La stratégie efficace la plus évidente consisterait à conserver une liste de clés doublement liée avec des références aux nœuds eux-mêmes stockés sous forme de valeurs dict (avec les valeurs réelles). Cela devient plus compliqué et l'implémente en pur Python porte beaucoup de frais généraux.
2
Mike Graham

Vous pouvez créer une classe de dictionnaire personnalisée en sous-classant dict. Dans votre cas, vous devrez remplacer __setitem__ pour vérifier votre propre longueur et supprimer quelque chose si la limite est recalculée. L'exemple suivant imprimerait la longueur actuelle après chaque insertion:

class mydict(dict):
    def __setitem__(self, k, v):
        dict.__setitem__(self, k, v)
        print len(self)

d = mydict()
d['foo'] = 'bar'
d['bar'] = 'baz'
1
c089

Il y a eu beaucoup de bonnes réponses, mais je veux souligner une implémentation Pythonic simple pour le cache LRU. C'est similaire à la réponse d'Alex Martelli.

from collections import OrderedDict, MutableMapping

class Cache(MutableMapping):
    def __init__(self, maxlen, items=None):
        self._maxlen = maxlen
        self.d = OrderedDict()
        if items:
            for k, v in items:
                self[k] = v

    @property
    def maxlen(self):
        return self._maxlen

    def __getitem__(self, key):
        self.d.move_to_end(key)
        return self.d[key]

    def __setitem__(self, key, value):
        if key in self.d:
            self.d.move_to_end(key)
        Elif len(self.d) == self.maxlen:
            self.d.popitem(last=False)
        self.d[key] = value

    def __delitem__(self, key):
        del self.d[key]

    def __iter__(self):
        return self.d.__iter__()

    def __len__(self):
        return len(self.d)
1
Ian Chen