summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJuan Batiz-Benet <jbenet@cs.stanford.edu>2011-07-10 05:04:20 -0700
committerJuan Batiz-Benet <jbenet@cs.stanford.edu>2011-07-10 05:55:07 -0700
commita96e83d7fb11c86349218eb2c22200d639665929 (patch)
tree246b098e86159b318b510f82454de2f19f7f6a4e
parent52fa54a4715705554ee3941e60d00ae0bdb9e773 (diff)
downloadpylru-a96e83d7fb11c86349218eb2c22200d639665929.tar.gz
removed stray tabs and stripped excess whitespace in pylru.py, test.py, and
README.txt
-rw-r--r--README.txt36
-rw-r--r--pylru.py139
-rw-r--r--test.py101
3 files changed, 139 insertions, 137 deletions
diff --git a/README.txt b/README.txt
index 384102b..6e3b0c3 100644
--- a/README.txt
+++ b/README.txt
@@ -15,7 +15,7 @@ You can install pylru, or you can just copy the source file pylru.py and use it
An LRU cache object has a dictionary like interface and can be used in the same way::
import pylru
-
+
size = 100
cache = pylru.lrucache(size)
@@ -27,57 +27,57 @@ An LRU cache object has a dictionary like interface and can be used in the same
cache.size() # Returns the size of the cache
cache.size(x) # Changes the size of the cache. x MUST be greater than
# zero.
-
+
x = len(cache) # Returns the number of elements stored in the cache.
- # x will be less than or equal to cache.size()
-
+ # x will be less than or equal to cache.size()
+
cache.clear() # Remove all elements from the cache.
-
+
The lrucache takes an optional callback function as a second argument. Since the cache has a fixed size some operations, such as an insertion, may cause a key/value pair to be ejected. If the optional callback function is given it will be called when this occurs. For example::
import pylru
-
+
def callback(key, value):
print (key, value) # A dumb callback that just prints the key/value
-
+
size = 100
cache = pylru.lrucache(size, callback)
-
+
# Use the cache... When it gets full some pairs may be ejected due to
# the fixed cache size. But, not before the callback is called to let you
# know.
-
+
Often a cache is used to speed up access to some other low latency object. If that object has a dictionary interface a convenience wrapper class provided by PyLRU can be used. This class takes as an argument the object you want to wrap and the cache size. It then creates an LRU cache for the object and automatically manages it. For example, imagine you have an object with a dictionary interface that reads/writes its values to and from a remote server. Let us call this object slowDict::
import pylru
-
+
size = 100
cacheDict = pylru.lruwrap(slowDict, size)
-
+
# Now cacheDict can be used just like slowDict, except all of the lookups
# are automatically cached for you using an LRU cache.
-
+
By default lruwrap uses write-through semantics. For instance, in the above example insertions are updated in the cache and written through to slowDict immediatly. The cache and the underlying object are not allowed to get out of sync. So only lookup performace can be improved by the cache. lruwrap takes an optional third argument. If set to True write-back semantics will be used. Insertions will be updated to the cache. The underlying slowDict will automatically be updated only when a "dirty" key/value pair is ejected from the cache.
The programmer is responsible for one thing though. They MUST call sync() when they are finished. This ensures that the last of the "dirty" entries in the cache are written back::
import pylru
-
+
size = 100
cacheDict = pylru.lruwrap(slowDict, size, True)
-
+
# Now cacheDict can be used just like slowDict, except all of the lookups
# are automatically cached for you using an LRU cache with Write-Back
# semantics.
# DON'T forget to call sync() when finished
cacheDict.sync()
-
+
To help the programmer with this the lruwrap can be used in a with statement::
-
+
with pylru.lruwrap(slowDict, size, True) as cacheDict
-
+
# Use cacheDict, sync() is called automatically for you when leaving the
# with statment block.
@@ -89,5 +89,5 @@ PyLRU also provides a function decorator::
@lrudecorator(100)
def square(x):
return x*x
-
+
# Now results of the square function are cached for future lookup.
diff --git a/pylru.py b/pylru.py
index 50dd597..8015f37 100644
--- a/pylru.py
+++ b/pylru.py
@@ -39,87 +39,90 @@
# stored under respectivly.
class _dlnode(object):
def __init__(self):
- self.key = None
+ self.key = None
+
+
class lrucache(object):
-
+
def __init__(self, size, callback=None):
-
+
self.callback = callback
# Initialize the hash table as empty.
self.table = {}
-
+
# Initalize the list with one empty node. Create a node and
# assign it to the 'head' variable, which represents the head node in the
# list.
self.head = _dlnode()
self.head.next = self.head
self.head.prev = self.head
-
+
self.listSize = 1
-
+
# Adjust the size
self.size(size)
def __len__(self):
return len(self.table)
-
+
# Does not call callback to write any changes!
def clear(self):
self.table.clear()
-
+
node = self.head
for i in range(self.listSize):
node.key = None
node.obj = None
node = node.next
-
-
+
+
def __contains__(self, key):
return key in self.table
# XXX Should this move the object to front of list? XXX
-
+
def peek(self, key):
# Look up the node
node = self.table[key]
return node.obj
-
+
+
def __getitem__(self, key):
-
+
# Look up the node
node = self.table[key]
-
+
# Update the list ordering. Move this node so that is directly proceeds the
# head node. Then set the 'head' variable to it. This makes it the new head
# of the list.
self.mtf(node)
self.head = node
-
+
# Return the object
return node.obj
-
-
+
+
def __setitem__(self, key, obj):
-
+
# First, see if any object is stored under 'key' in the cache already. If
# so we are going to replace that object with the new one.
if key in self.table:
-
+
# Lookup the node
node = self.table[key]
-
+
# Replace the object
node.obj = obj
-
+
# Update the list ordering.
self.mtf(node)
self.head = node
-
+
return
-
+
# Ok, no object is currently stored under 'key' in the cache. We need to
# choose a node to place the object 'obj' in. There are two cases. If the
# cache is full some object will have to be pushed out of the cache. We
@@ -129,44 +132,44 @@ class lrucache(object):
# nodes are always together at the tail end of the list. Thus, in either
# case, by chooseing the node at the tail of the list our conditions are
# satisfied.
-
+
# Since the list is circular, the tail node directly preceeds the 'head'
# node.
node = self.head.prev
-
+
# If the node already contains something we need to remove the old key from
# the dictionary.
if node.key is not None:
if self.callback:
self.callback(node.key, node.obj)
del self.table[node.key]
-
+
# Place the new key and object in the node
node.key = key
node.obj = obj
-
+
# Add the node to the dictionary under the new key.
- self.table[node.key] = node
-
+ self.table[node.key] = node
+
# We need to move the node to the head of the list. The node is the tail
# node, so it directly preceeds the head node due to the list being
# circular. Therefore, the ordering is already correct, we just need to
# adjust the 'head' variable.
self.head = node
-
-
+
+
def __delitem__(self, key):
-
+
# Lookup the node, then remove it from the hash table.
node = self.table[key]
del self.table[key]
-
+
# Set the 'key' to None to indicate that the node is empty. We also set the
# 'obj' to None to release the reference to the object, though it is not
# strictly necessary.
node.key = None
node.obj = None
-
+
# Because this node is now empty we want to reuse it before any non-empty
# node. To do that we want to move it to the tail of the list. We move it
# so that it directly preceeds the 'head' node. This makes it the tail
@@ -175,30 +178,30 @@ class lrucache(object):
self.mtf(node)
self.head = node.next
-
-
+
+
def size(self, size=None):
-
+
if size is not None:
assert size > 0
if size > self.listSize:
self.addTailNode(size - self.listSize)
elif size < self.listSize:
self.removeTailNode(self.listSize - size)
-
+
return self.listSize
-
- # Increases the size of the cache by inserting n empty nodes at the tail of
+
+ # Increases the size of the cache by inserting n empty nodes at the tail of
# the list.
def addTailNode(self, n):
for i in range(n):
node = _dlnode()
node.next = self.head
node.prev = self.head.prev
-
+
self.head.prev.next = node
self.head.prev = node
-
+
self.listSize += n
# Decreases the size of the list by removing n nodes from the tail of the
@@ -211,33 +214,33 @@ class lrucache(object):
if self.callback:
self.callback(node.key, node.obj)
del self.table[node.key]
-
+
# Splice the tail node out of the list
self.head.prev = node.prev
node.prev.next = self.head
-
+
# The next four lines are not strictly necessary.
node.prev = None
node.next = None
-
+
node.key = None
node.obj = None
-
+
self.listSize -= n
-
-
+
+
# This method adjusts the doubly linked list so that 'node' directly preeceds
# the 'head' node. Note that it works even if 'node' already directly
# preceeds the 'head' node or if 'node' is the 'head' node, in either case
# the order of the list is unchanged.
def mtf(self, node):
-
+
node.prev.next = node.next
node.next.prev = node.prev
node.prev = self.head.prev
node.next = self.head.prev.next
-
+
node.next.prev = node
node.prev.next = node
@@ -257,44 +260,44 @@ class lruwrap(object):
self.store[key] = value
self.dirty.remove(key)
self.cache = lrucache(size, callback)
-
+
def __len__(self):
return len(self.store)
-
+
def size(self, size=None):
self.cache.size(size)
-
+
def clear(self):
self.cache.clear()
self.store.clear()
if self.writeback:
self.dirty.clear()
-
+
def __contains__(self, key):
# XXX Should this bring the key/value into the cache?
if key in self.cache:
return True
if key in self.store:
return True
-
+
return False
-
+
def __getitem__(self, key):
try:
return self.cache[key]
except KeyError:
pass
-
+
return self.store[key] # XXX Re-raise exception?
-
+
def __setitem__(self, key, value):
self.cache[key] = value
-
+
if self.writeback:
self.dirty.add(key)
else:
self.store[key] = value
-
+
def __delitem__(self, key):
if self.writeback:
found = False
@@ -304,13 +307,13 @@ class lruwrap(object):
self.dirty.remove(key)
except KeyError:
pass
-
+
try:
del self.store[key]
found = True
except KeyError:
pass
-
+
if not found: # If not found in cache or store, raise error.
raise KeyError
@@ -320,18 +323,18 @@ class lruwrap(object):
del self.cache[key]
except KeyError:
pass
-
-
+
+
def sync(self):
if self.writeback:
for key in self.dirty:
value = self.cache.peek(key) # Doesn't change the cache's order
self.store[key] = value
self.dirty.clear()
-
+
def __enter__(self):
return self
-
+
def __exit__(self, exc_type, exc_val, exc_tb):
self.sync()
return False
@@ -340,14 +343,14 @@ class lruwrap(object):
class lrudecorator(object):
def __init__(self, size):
self.cache = lrucache(size)
-
+
def __call__(self, func):
def wrapped(*args): # XXX What about kwargs
try:
return self.cache[args]
except KeyError:
pass
-
+
value = func(*args)
self.cache[args] = value
return value
diff --git a/test.py b/test.py
index 8ed5fee..ca107f4 100644
--- a/test.py
+++ b/test.py
@@ -6,36 +6,36 @@ import random
# results against another, simpler, LRU cache implementation.
class simplelrucache:
-
+
def __init__(self, size):
-
+
# Initialize the cache as empty.
self.cache = []
self.size = size
-
+
def __contains__(self, key):
-
+
for x in self.cache:
if x[0] == key:
return True
-
+
return False
-
-
+
+
def __getitem__(self, key):
-
+
for i in range(len(self.cache)):
x = self.cache[i]
if x[0] == key:
del self.cache[i]
self.cache.append(x)
return x[1]
-
+
raise KeyError
-
-
+
+
def __setitem__(self, key, obj):
-
+
for i in range(len(self.cache)):
x = self.cache[i]
if x[0] == key:
@@ -43,33 +43,33 @@ class simplelrucache:
del self.cache[i]
self.cache.append(x)
return
-
+
if len(self.cache) == self.size:
self.cache = self.cache[1:]
-
+
self.cache.append([key, obj])
-
-
+
+
def __delitem__(self, key):
-
+
for i in range(len(self.cache)):
if self.cache[i][0] == key:
del self.cache[i]
return
-
+
raise KeyError
-
+
def test(a, b, c, d, verify):
-
+
for i in range(1000):
x = random.randint(0, 512)
y = random.randint(0, 512)
-
+
a[x] = y
b[x] = y
verify(c, d)
-
+
for i in range(1000):
x = random.randint(0, 512)
if x in a:
@@ -79,7 +79,7 @@ def test(a, b, c, d, verify):
else:
assert x not in b
verify(c, d)
-
+
for i in range(256):
x = random.randint(0, 512)
if x in a:
@@ -89,8 +89,8 @@ def test(a, b, c, d, verify):
else:
assert x not in b
verify(c, d)
-
-
+
+
def testcache():
def verify(a, b):
q = []
@@ -98,40 +98,40 @@ def testcache():
for j in range(len(a.table)):
q.append([z.key, z.obj])
z = z.next
-
+
assert q == b.cache[::-1]
-
-
+
+
a = lrucache(128)
b = simplelrucache(128)
-
+
test(a, b, a, b, verify)
-
+
def wraptest():
-
+
def verify(p, q):
assert p == q
-
+
p = dict()
q = dict()
x = lruwrap(q, 128)
-
+
test(p, x, p, q, verify)
-
-
-
+
+
+
def wraptest2():
def verify(x, y):
pass
-
+
p = dict()
q = dict()
x = lruwrap(q, 128, True)
-
+
test(p, x, None, None, verify)
-
+
x.sync()
assert p == q
@@ -139,33 +139,33 @@ def wraptest3():
def verify(x, y):
pass
-
+
p = dict()
q = dict()
with lruwrap(q, 128, True) as x:
test(p, x, None, None, verify)
-
+
assert p == q
-
-
+
+
@lrudecorator(25)
def square(x):
return x*x
-
+
def testDecorator():
for i in range(1000):
x = random.randint(0, 1493)
assert square(x) == x*x
-
-
-
-
+
+
+
+
if __name__ == '__main__':
-
+
random.seed()
-
+
for i in range(20):
testcache()
wraptest()
@@ -174,4 +174,3 @@ if __name__ == '__main__':
testDecorator()
-