Commits

Jonathan Eunice committed feb9b3a

initial commit as separate projects

Comments (0)

Files changed (4)

+syntax: glob
+*.swp.{py,txt,html,css,js}
+*.pyc
+.DS_Store
+build/*
+dist/*
+*.egg-info
+setup.cfg
+PKG-INFO
+Classes that use this caching metaclass will have their instances
+automatically cached based on instantiation-time arguments (i.e. to ``__init__``).
+Useful for not repetitively creating expensive-to-create objects,
+and for making sure that objects are created only once.
+
+Usage
+=====
+
+Say you have a class ``Thing`` that requires expensive computation to
+create, or that should be created only once. You can make that happen
+simply by adding one line to its definition::
+
+    from mementos import MementoMetaclass
+
+    class Thing(object):
+        
+        __metaclass__ = MementoMetaclass
+        
+        def __init__(self, name):
+            self.name = name
+        
+        ...
+
+Then ``Thing`` objects will be memoized::
+
+    t1 = Thing("one")
+    t2 = Thing("one")
+    assert t1 is t2
+    
+
+Notes
+=====
+
+ *  Derived from `an ActiveState recipe
+    <http://code.activestate.com/recipes/286132-memento-design-pattern-in-python/>`_ by Valentino Volonghi.
+    
+ *  It is safe to memoize multiple classes at the same time. They will all be stored in the same cache, but
+    their class is a part of the cache key, so the values are distinct.
+   
+ *  This implementation is *not* thread-safe, in and of itself. If you're
+    in a multi-threaded environment, wrap object instantiation in a lock.
+   
+ *  Be careful with call signatures. This metaclas
+    caches on call signature, which can vary if keyword args
+    are used. E.g. ``def func(a, b=2)`` could be called ``func(1)``, ``func(1,2)``,
+    ``func(a=1)``, ``func(1, b=2)``, or ``func(a=2, b=2)``--and all resolve to the
+    same logical call. And this is just for two parameters! If there are more
+    than one kwarg, they can be arbitrarily ordered, creating *many* logically
+    identical permuations. Thank Goodness Python doesn't allow kwargs to come
+    before positional args, else there'd be even more ways to make the same
+    call.
+    
+    So if you instantiate an object once, then again with a logically
+    identical call but using a different calling structure/signature, the
+    object won't be created and cached just once--it will be created and
+    cached multiple times.::
+    
+        o1 = Thing("lovely")
+        o2 = Thing(name="lovely")
+        assert o1 is not o2   # because the call signature is different
+        
+    This may degrade performance, and can also create
+    errors, if you're counting on ``mementos`` to create just one object.
+    So don't do that. Use the same calling style, and it won't be a problem.
+    
+    In most cases, this isn't an issue, because objects tend to be
+    instanitated with a limited number of parameters, and you can take care
+    that you instantiate them with parallel call signatures. Since this works
+    99% of the time and has a simple implementation, it's worth the price of
+    this inelegance. For the 1% edge-cases where multiple call signature
+    variations must be conclusively resolved to a unique canonical signature,
+    the ``inspect`` module could be used (e.g. ``getargvalues()`` or
+    ``getcallargs()`` to create such a unified key.
+    
+ *  Not to be confused with the `memento <http://pypi.python.org/pypi/memento>`_
+    module, which does something completely different.
+    
+ *  In some cases, you may not want the entire initialization-time call signature
+    to define the cache key--only part of it. (Because some arguments to ``__init__``
+    may be useful, but not really part of the object's 'identity'.) The
+    best option is to design ``__init__`` without superflous attributes, then
+    create a second
+    method that adds sets useful-but-not-essential data. E.g.::
+
+        class OtherThing(object):
+                
+            __metaclass__ = MementoMetaclass
+            
+            def __init__(self, name):
+                self.name = name
+                self.color = None
+                self.weight = None
+                
+            def set(self, color=None, weight=None):
+                self.color = color or self.color
+                self.weight = weight or self.weight
+                return self
+        
+        ot1 = OtherThing("one").set(color='blue')
+        ot2 = OtherThing("one").set(weight='light')
+        assert ot1 is ot2
+        assert ot1.color == ot2.color == 'blue'
+        assert ot1.weight == ot2.weight == 'light'
+
+Installation
+============
+
+::
+
+    pip install mementos
+    
+(You may need to prefix this with "sudo " to authorize installation.)
+class MementoMetaclass(type):
+    """
+    Classes that use this caching metaclass will have their instances
+    automatically cached based on instantiation-time arguments (i.e. to __init__).
+    Super-useful for not repetitively creating expensive-to-create objects.
+    
+    See http://code.activestate.com/recipes/286132-memento-design-pattern-in-python/
+    """
+    cache = {}
+
+    def __call__(self, *args, **kwargs):
+        key = (self, ) + args + tuple(kwargs.items())
+        try:
+            return self.cache[key]
+        except KeyError:
+            instance = type.__call__(self, *args, **kwargs)
+            self.cache[key] = instance
+            return instance
+
+
+if __name__ == '__main__':
+    
+    class Thing(object):
+        
+        __metaclass__ = MementoMetaclass
+        
+        def __init__(self, name):
+            self.name = name
+        
+
+    t1 = Thing("one")
+    t2 = Thing("one")
+    assert t1 is t2
+    
+    o1 = Thing("lovely")
+    o2 = Thing(name="lovely")
+    assert o1 is not o2   # because the call signature is different
+    
+    class OtherThing(object):
+            
+        __metaclass__ = MementoMetaclass
+        
+        def __init__(self, name):
+            self.name = name
+            self.color = None
+            self.weight = None
+            
+        def set(self, color=None, weight=None):
+            self.color = color or self.color
+            self.weight = weight or self.weight
+            return self
+    
+    ot1 = OtherThing("one").set(color='blue')
+    ot2 = OtherThing("one").set(weight='light')
+    assert ot1 is ot2
+    assert ot1.color == ot2.color == 'blue'
+    assert ot1.weight == ot2.weight == 'light'
+
+    print "working as designed!"
+    
+#! /usr/bin/env python
+
+from setuptools import setup
+from decimal import Decimal
+import re
+
+def linelist(text):
+    """
+    Returns each non-blank line in text enclosed in a list.
+    """
+    return [ l.strip() for l in text.strip().splitlines() if l.split() ]
+    
+    # The double-mention of l.strip() is yet another fine example of why
+    # Python needs en passant aliasing.
+
+
+def verno(s):
+    """
+    Update the version number passed in by extending it to the 
+    thousands place and adding 1/1000, then returning that result
+    and as a side-effect updating setup.py
+
+    Dangerous, self-modifying, and also, helps keep version numbers
+    ascending without human intervention.
+    """
+    d = Decimal(s)
+    increment = Decimal('0.001')
+    d = d.quantize(increment) + increment
+    dstr = str(d)
+    setup = open('setup.py', 'r').read()
+    setup = re.sub('verno\(\w*[\'"]([\d\.]+)[\'"]', 'verno("' + dstr + '"', setup)
+    open('setup.py', 'w').write(setup)
+    return dstr
+
+setup(
+    name='mementos',
+    version=verno("0.105"),
+    author='Jonathan Eunice',
+    author_email='jonathan.eunice@gmail.com',
+    description='Memoizing metaclass. Drop-dead simple way to create cached objects',
+    long_description=open('README.rst').read(),
+    url='',
+    py_modules=['mementos'],
+    install_requires=[],
+    classifiers=linelist("""
+        Development Status :: 4 - Beta
+        Operating System :: OS Independent
+        License :: OSI Approved :: BSD License
+        Intended Audience :: Developers
+        Programming Language :: Python
+        Topic :: Software Development :: Libraries :: Python Modules
+    """)
+)