I'm doing a lot of web2py programming lately, and much of my time is spent building helper classes that act as "factories" for high-powered web gadgets. i have a class that builds jQuery DataTables, for example, and another that builds "widgets" as defined in my templating system (each of which is an elaborate construct of header, toolbar, body, etc.).
It occurred to me that many of these objects were by nature a "singleton" -- that is, a single instance of the object (the factory in this case) could be used over and over. The alternative is to create a new instance and initialize it each time you use it -- and since it is used several times on each page, that means a lot of instances created and destroyed.
So how do you make something a singleton? Step Zero is to make sure we are building these classes in a "module" instead of a model or a controller. These must be compiled for their magic goodness to work properly. Otherwise, there's no savings of effort, but the opposite!
Step one is to ensure there is nothing in the object's instance vars which is unique to that instance. In other words, anything that changes from one use of the object to the next must be passed in from outside. In my case the widgets are all driven from the database, so they don't hold any internal state to worry about.
Next, we need to determine how unique these instances are -- is there only one? Is there potentially a different one on each distinct web page (controller/function)? We use this to create a dictionary of instances that stores the object we create for each page in between invocations.
Last, we implement a "metaclass" that take care of the lookup of the singleton instance, or creates a new one if the singleton dictionary is empty. The metaclass is the cleanest way of creating a Singleton. There are other patterns in Python, but they all suffer from one problem or another. Some replace the class object with a function -- those won't work if you subclass anything. Of the three or four patterns I tried, this is the best.
Singleton class definition:
#!/usr/bin/env python
# coding: utf8
from gluon.globals import current
_instances = {}
class Singleton(type):
def __call__(cls, *args, **kwargs):
cr = current.request
key = ('/').join([cr.controller,cr.function])
if key not in _instances:
_instances[key] = super(Singleton, cls).__call__(*args, **kwargs)
return _instances[key]
This class keeps a dictionary of instances and uses a key of "contoller/function" from the current request to look up the Singleton. Other dictionary keys are useful in other scenarios. Also note that Singleton is a subclass of "type", not "object" as you may expect.
To make a class (or even a superclass of classes) a Singleton, just declare the metaclass like so:
class WidgetHandler(object):
if current.settings.singleton:
__metaclass__ = Singleton
def __init__(self, controller=None, title='', id=None, theme=None ):
from gluon.storage import Storage
from fcn import make_theme
self.controller = controller
self.params = Storage()
self.title = title
self.theme = make_theme(theme)
self.api = ['widget','content']
self.scripts = []
self.html_cache = None
self.script_cache = None
self.init_datatables()
The critical business is highlighted. just declare this class to use "Singleton" as metaclass and it will magically become a Singleton. I left the __init__ showing so you can see how elaborate initialization can be. The call to "init_datatables" creates and initializes a jQuery Datatables object that, itself, requires a lot of steps. It then builds the HTML representation of the widget, the javascripts that go with it, and so forth. All of that effort is saved by bypassing this initialization each time.
In my case, WidgetHandler is itself used as a superclass by numerous WidgetXXX classes. It holds the common template and reduces the amount of code in each widget class. It also provides a single point of control for all of the common code. Singleton "decorator" patterns do not work when creating a superclass because the class name is replaced with a function that generates a class, so super() fails to work properly.
Note the use of the global option "current.settings.singleton" to turn the Singleton feature off for debugging. This will save some hair pulling as you work on the code, expecting it to use the code you just edited when the object has been cached instead! It also provides an easy way to compare speed and memory usage with and without the Singleton feature.
Here is a sample of a widget happily sitting on its web page. It is quick to load and refresh, because (a) it's compiled into a module and (b) it's a singleton!