#! /usr/bin/env python # -*- coding: utf-8 -*- # Copyright © 2008 Neil Moore . # # 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 2 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, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA class FirstArg(type): """Metaclass to cache object creation by a key, which must be the first constructor parameter. >>> import cacher >>> class MyClass (object): ... __metaclass__ = cacher.FirstArg ... def __init__(self, name, extra): ... self.name = name ... self.extra = extra >>> obj = MyClass('foo', 42) The constructor must be called with the correct number of arguments: >>> bad = MyClass('bad') Traceback (most recent call last): .... TypeError: __init__() takes exactly 3 arguments (2 given) Subsequent calls to the constructor with the same first argument result in the same object: >>> MyClass('foo', 42) is obj True This is true even if the other parameters differ: >>> MyClass('foo', None) is obj True >>> MyClass('foo', 'twelve').extra 42 In fact, if an instance has already been constructed, the remaining parameters are disregarded entirely, and may be omitted: >>> MyClass('foo') is obj True >>> MyClass('foo', 1, 2, 3, 4, 5, 6, 7) is obj True If the first parameter differs, the constructed object will differ: >>> MyClass('bar', 18) is obj False >>> MyClass('bar').extra 18 Any immutable key may be used: >>> pairobj = MyClass((1,2), {}) >>> MyClass((1,2)).name (1, 2) >>> MyClass((1,2)).extra {} >>> badobj = MyClass([1,2], 26) Traceback (most recent call last): .... TypeError: list objects are unhashable However, the key None will not be cached: >>> noneobj = ( MyClass(None, 'first'), MyClass(None, 'second') ) >>> noneobj[0].extra 'first' >>> noneobj[1].extra 'second' The objects themselves can be mutable: >>> MyClass('foo').extra = 9 >>> obj.extra 9 >>> MyClass('bar').name = 'something else' >>> MyClass('bar').name 'something else' The cache is not shared with subclasses: >>> class SubClass (MyClass): ... def __init__(self, name, extra): ... super(SubClass,self).__init__(name, extra) >>> sub = SubClass('foo', 76) >>> sub is obj False >>> sub.extra 76 """ def __init__(cls, name, bases, cdict): super(FirstArg, cls).__init__(name, bases, cdict) cls.__instcache = {} def __call__(cls, name, *args, **kw): if name is None or name not in cls.__instcache: cls.__instcache[name] = super(FirstArg, cls).__call__( name, *args, **kw ) return cls.__instcache[name] class Singleton (type): """Metaclass for singleton objects. After the first constructor call initializes an object, subsequent calls will return the same object. >>> import cacher >>> class MyClass (object): ... __metaclass__ = cacher.Singleton ... def __init__(self, data): ... self.data = data The constructor must be called with the correct number of arguments: >>> bad = MyClass() Traceback (most recent call last): .... TypeError: __init__() takes exactly 2 arguments (1 given) >>> obj = MyClass('only') After an object is successfully constructed, subsequent calls to the constructor yield the same object: >>> MyClass('only') is obj True This is true even if the other parameters differ: >>> MyClass('another') is obj True >>> MyClass('ignored').data 'only' In fact, once an instance has been constructed, subsequent calls to the constructor ignore the parameters entirely: >>> MyClass() is obj True >>> MyClass(1, 2, 3, 4, 5, 6, 7) is obj True The objects can be mutable: >>> MyClass().data = 'changed' >>> obj.data 'changed' The cache is not shared with subclasses: >>> class SubClass (MyClass): ... def __init__(self, data): ... super(SubClass,self).__init__(data) >>> sub = SubClass('foo') >>> sub is obj False >>> sub.data 'foo' >>> MyClass('foo').data 'changed' """ def __init__(cls, name, bases, cdict): super(Singleton, cls).__init__(name, bases, cdict) cls.__instance = None def __call__(cls, *args, **kw): if cls.__instance is None: cls.__instance = super(Singleton, cls).__call__(*args, **kw) return cls.__instance if __name__ == "__main__": import doctest doctest.testmod()