summaryrefslogtreecommitdiff
path: root/larch/lru.py
blob: 5e2514981aa45c4e1ea833cb2286b5f88808c737 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# Copyright 2010  Lars Wirzenius, Richard Braakman
# 
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.


import heapq
import logging


class LRUCache(object):

    '''A least-recently-used cache.

    This class caches objects, based on keys. The cache has a fixed size,
    in number of objects. When a new object is added to the cache, the
    least recently used old object is dropped. Each object is associated
    with a key, and use is defined as retrieval of the object using the key.
    
    Two hooks are provided for: for removing an object by user request, 
    and when it is automatically removed due to cache overflow. Either
    hook is called with the key and object as arguments.

    '''

    def __init__(self, max_size, remove_hook=None, forget_hook=None):
        self.max_size = max_size
        # Together, obj_before and obj_after form a random access
        # double-linked sequence. None used as the sentinel on both ends.
        self.obj_before = dict()
        self.obj_after = dict()
        self.obj_before[None] = None
        self.obj_after[None] = None
        self.ids = dict()  # maps key to object
        self.objs = dict() # maps object to key
        self.remove_hook = remove_hook
        self.forget_hook = forget_hook
        self.hits = 0
        self.misses = 0

    def log_stats(self): # pragma: no cover
        logging.debug('LRUCache %s: hits=%s misses=%s' %
                      (self, self.hits, self.misses))

    def __len__(self):
        return len(self.ids)

    def keys(self):
        '''List keys for objects in cache.'''
        return self.ids.keys()

    def add(self, key, obj):
        '''Add new item to cache.'''
        if key in self.ids:
            self.remove(key)
        before = self.obj_before[None]
        self.obj_before[None] = obj
        self.obj_before[obj] = before
        self.obj_after[before] = obj
        self.obj_after[obj] = None
        self.ids[key] = obj
        self.objs[obj] = key
        while len(self.ids) > self.max_size:
            self._forget_oldest()

    def _forget_oldest(self):
        obj = self.obj_after[None]
        key = self.objs[obj]
        self._remove(key)
        if self.forget_hook:
            self.forget_hook(key, obj)
        
    def _remove(self, key):
        obj = self.ids[key]
        before = self.obj_before[obj]
        after = self.obj_after[obj]
        self.obj_before[after] = before
        self.obj_after[before] = after
        del self.obj_before[obj]
        del self.obj_after[obj]
        del self.ids[key]
        del self.objs[obj]
        
    def get(self, key):
        '''Retrieve item from cache.

        Return object associated with key, or None.

        '''
        
        if key in self.ids:
            self.hits += 1
            obj = self.ids[key]
            self.remove(key)
            self.add(key, obj)
            return obj
        else:
            self.misses += 1
            return None
        
    def remove(self, key):
        '''Remove an item from the cache.
        
        Return True if item was in cache, False otherwise.
        
        '''

        if key in self.ids:
            obj = self.ids[key]
            self._remove(key)
            if self.remove_hook:
                self.remove_hook(key, obj)
            return True
        else:
            return False

    def remove_oldest(self):
        '''Remove oldest object.
        
        Return key and object.
        
        '''

        obj = self.obj_after[None]
        key = self.objs[obj]
        self.remove(key)
        return key, obj