Design Patterns and Python
Flyweight Pattern
Presentation
From the Gang of Four, a flyweight is a shared object that
can be used in multiple contexts simultaneously
and must be
independent of its context. Thus, only sharable state (intrinsic) must
be stored in the flyweight object itself, the context dependant state
(extrinsic) being kept separately and passed to the flyweight when
needed.
Here, we will only consider the flyweight creation mechanic, that is
managing shared objects, and thus
only deal with intrinsic state. This pattern is usually used when the
application needs a large number of instances of the same object having the
same intrinsic state, whose extrinsic state can be computed (or its storage
space is low), in order to limit the memory used by these objects.
The implementation presented in the GoF uses a factory class to create and manage flyweight instances, needing a specific factory for each flyweight interface. In this case, the factory class keep track of already created instances, and delegates the instances creation if needed to the flyweight class itself. This is the first approach presented here. However, this approach, being well suited to static languages (like Java or C++), is a too heavy one for dynamic languages like Python. Indeed, thanks to the Python duck typing, the factory mechanic can also be added to the class itself, using inheritance or dynamic class modification, which allow for a more generic approach. Language aspects such as decorators, multiple inheritance and mixins, and metaclasses make such approaches quite elegant to implement and easy to use.
The code snippets presented here are not usable as-is, since some aspects are not taken into account (e.g. subclassing a flyweight). See the discussion part for more details.
Strict Gang of Four implementation
This is a quite strict direct implementation of the pattern as described in the GoF, without using some nice features of Python. This is how this could be implemented in a static and strongly typed language.
However, since Python is dynamically typed and classes are first class citizen
objects, this approach can easily be generalized to be more generic. By using
the *args
magic in the get_instance
method, instead
of the explicit constructor parameters, and passing the flyweight class
as a factory constructor parameter, the factory itself can be generic, that is
independent of the flyweight object class, leading to the following
version.
Gang of Four Version
This version is just a more pythonic version of the GoF one. By using the
*args
magic and passing the class to be instantiated to the factory
constructor, there is no need to create a specific factory for each
flyweight class.
Indeed, there is no need for the Abstract Factory pattern, since
classes, as first class objects, can be passed as parameters.
The factory is therefore generic using parametric polymorphism.
The class to be
instantiated by the factory is therefore not hardcoded, kept in the _cls
attribute, and directly called in get_instance
.
The *args
magic allow the functions or methods to accept variable
arguments. Here, get_instance
can therefore accept any arguments
that are passed as is to the _cls
class to create the instance if
needed.
One more difference with the previous code is the use of
setdefault
. This dict
method does just what the test
did, that is, set the value of the corresponding key if it does not exists, and
return the value. Since args
is a list, we just need to convert it
to a tuple to use it as dict key (since keys must be immutable).
Wrapping Decorator Version
The next step is the use of the __call__
special method. Every
instance (whatever the type) having a __call__
method is
callable, that is can be used as a function. By renaming
get_instance
into __call__
, we can thus instantiate
Spam
using SpamFactory(1, 2)
directly.
However, the global instantiation of FlyweightFactory
into
e.g. SpamFactory
is not as elegant as it could be.
Indeed, since the instance is directly called to obtain an Spam
instance (we don’t use get_instance
anymore), from the user
point of view, it behave like the Spam
class itself. Moreover, in
this pattern spirit, the Spam
class should
not be called (instantiated) directly to guaranty that we get the same
instances. One way would be to mask the Spam
class by replacing it
with the SpamFactory
instance:
Spam = FlyweightFactory(Spam)
.
The flyweight machinery is therefore transparent to the user; but the
FlyweightFactory
still
have to be instantiated. Hopefully, this is exactly what class
decorators are for.
Python’s decorators are high order callables, that is a callable
taking a callable as parameter and returning a callable; these callable can be
functions, a classes, etc. This is exactly the behaviour of our previous
FlyweightFactory
class. Moreover, some syntactic sugar allow to
easily replace a callable with the decorated one, masking the original: the
@
notation.
In this code, the @flyweight
decoration is equivalent to the
previous Spam = flyweight(Spam)
.
Functional Wrapping Decorator
This begin to be quite usable. However, we can note that the
flyweight
class corresponds to the method object pattern.
In functional languages, this pattern can be simply
implemented using a closure (see method
object).
Here, the instances dictionary and the class to be instantiated are closed
in the lambda function definition instead of keeping them in objects
attributes. The resulting decorated class (e.g. Spam
) is actually
a closure wrapping the original class, that when called create a new instance
if needed and returns it.
Note how this last generic decorator approach is concise, elegant an easy to use compared to the specific original one.
Despite the zen of Python stating that
there should be one— and
preferably only one —obvious way to do it
, the decorator approach is
not the only one that can be generic and elegant in python. One of them is the
mixin approach we will now examine.
GoF Mixin Version
Until then, we have had a delegation approach, creating a class or a
function that wraps the actual class, keeping track of instances and delegating
the intantiation. However, a approach similar to the singleton
pattern, i.e. keeping the list of instances in a static attribute of
the class, can be used. In this case, the factory method
get_instance
is a static one, the default constructor beeing
private to ensure that the user use this method to create instance.
In python vocabulary, this static method is called a class method,
since the class is the implicit first argument (python static methods do not
have implicit arguments), and is defined using the @classmethod
decorator.
Again, thanks to the *args
magic and the dynamic typing
of the instances dictionary content, we can generalize this to avoid to add the
_instances
class attribute and the get_instances
class method to each class we want to be flyweight.
Here, we use inheritance to add the class properties to the
flyweight classes. The FlyweightMixin
class is a mixin,
since it only implement specific functionalities (the flyweight machinery), and
is not a complete class on its own.
Since the _instances
dictionary is defined in the mixin, it is a shared attribute among all classes
using it, so the previous code for get_instance
must be modified.
Indeed, the constructor arguments are not enough to identify the instance, we
also need to know its class. An other solution is to redefine the
_instances
class attribute in every class using the mixin, but it
is more tedious and error prone. Since get_instance
is a class
method, its first argument is the class itself (not the mixin thanks to
polymorphism and dynamic dispatch), it is easy to add it to the dictionary
key. This is checked in the sample code by the last assert
.
Finally, this cls
parameter is also used to instantiate the
class. Note that here, it is a implicit parameter of the class method by
polymorphism, whereas is the delegation version, it was a parameter of the
wrapping object.
Mixin class
However, this version is not fully satisfactory, since nothing prevents the
user to directly instantiate the class, bypassing the flyweight, since the
python constructor __init__
can’t be private. Actually,
__init__
is just the second step in the instantiation process, the
one initializing the instance self
(hence its name), the first one (the creation of
the instance itself) occurring in the special class method
__new__
.
By moving the flyweight logic from get_instance
to
__new__
, we can prevent this bypassing. Moreover, since the
flyweight logic invocation is now transparent, we have a more readable syntax.
Since we are overriding __new__
, the instantiation must call
the super class method (here object
) to avoid a circular call. The
use of super
allow better extensibility than
object.__new__(cls, *args, **kargs)
. Note that since
__new__
is a special method defined to be a class method, the
@classmethod
decorator is no longer necessary.
Modifying Decorator Version
Besides inheritance, the class properties can be added to the flyweight by directly modifying the class itself. Indeed, since python is a fully dynamic language, classes can be changed at runtime. This is a more dynamic, less restrictive and, in my sense, more elegant approach than the mixin one, since it can be used on existing classes (e.g. third party classes), and can easily be accomplished using a class decorator.
Contrary to the wrapping decorator,
the function that create instances (_get_instance
) is
defined outside the decorator instead of in a anonymous function
(lambda
). This is possible since here it is not a closure but will
be dynamically bound as a class method, and
avoid the creation of a new function for every decorated class.
The decorator return the class itself, after modification, and not a wrapped one, which is cleaner from the class metadata point of view (see discussion).
Since the instances dictionary is now added to each class, the class itself is no more used in the key, but
super
is still used since the function is used to redefine the
decorated class __new__
method. For the same reason, the function
is explicitly defined as a class method. This is necessary since it is added to the class
after its creation, it would be unbounded if not marked as such.
Note that the decorator modify the class, and thus use side effects.
However, the class itself is returned. This is usually bad practice, but it is
required in the context of decorator, since the result of the decoration action
is the returned value. If the class were not returned, Spam
(the variable) would result to be None
, the default return value for python
functions, and not the modified class, which would be inaccessible.
However, thanks to this side effect, one can modify an existing class by
calling flyweight
on it without re-affecting the class name, as in
flyweight(ExistingClass)
.
To be strict, calling the flyweight
function a
decorator is abusive, since it does not decorate the class in the
eponymous pattern sense, but modify it. This designation come from it’s use
with the @
syntax, and is usually admitted in the Python
community.
Metaclass Version
An other way to add properties to a class without explicitly putting them in
its definition is to use a metaclass. A metaclass is the class that a
class is an instance of: Class
in Java, type
in
python; that is the class that govern the creation of the class itself.
Contrary to Java, where Class
is final and there is no
(easy) way to specify an alternate metaclass for a class, this is quite easy in
python, by subtyping type
and using the __metaclass__
special class attribute (in Python < 3; for newer version, the metaclass is
specified using the metaclass
class parameter).
Since the _get_instance
method is an instance method of the
metaclass, it will be a class method of the flyweight class,
without decorating it with @classmethod
as in
the mixin version. If done, it would be a metaclass class method, that is
a method taking the metaclass as first argument.
The __init__
method
initialise the created class, adding it the __instances
dictionary,
and making its __new__
method the same as its
_get_instance
method, giving the same behaviour as the mixin
version, but without using inheritance. The __init__
method works
exactly like the modifying decorator, but does not need to return the class,
since it’s a initialization method, working by side effect.
The type.__init__
call is
a call to the super class of our metaclass, here type
, which is
the type for all python types, classes included. This call allow the proper
initialization of our instance, the flyweight class.
By affecting this flyweight metaclass to the __metaclass__
attribute of a class (e.g. Spam
), it became a
flyweight. The metaclass is automatically instantiated when the class
is created.
Functional Metaclass Version
Strictly speaking, from the python point of view, any callable returning a
class can be used as a metaclass, not only type
subclasses. Since
type
can be called directly to create classes, creating such a
function is quite easy.
The parameters for type
are the class name, the class parents,
and a dictionary containing class properties (attributes and methods). These
parameters are automatically extracted from the class definition and provided
to the type
function or to the __metaclass__
callable when python
create the class.
Here, we create the class as defined, calling type
with the
unmodified parameters, and modify the result afterwards, adding the
__instances
dictionary and the __new__
method. This
approach is kind of an hybrid between the classical
metaclass and the modifying decorator.
Pure functional metaclass
An other way to create the class with the added features would be to modify
the last argument containing the class properties (here attrs
) to
add it __instances
and __new__
before calling
type
. The same variant can also be applied in the
MetaFlyweight
metaclass __init__
method.
This is the approach used in this last version, where the
__metaclass__
attribute is set to a function that create the class
by calling type
with modified arguments. Just to illustrate the
functional aspect, this function is defined as a lambda
(anonymous function),
with the properties added in place. This version is just an illustration, and I
would not want to have to maintain it!
Discussion
Concerning subclassing and introspection
If we need to subclass a flyweight class or use introspection on it, the wrapping approaches are not valid. Indeed, in these versions, we don’t have a direct access to the initial class, but to a function (or to the wrapping class) returning an instance. The class metadata (type, docstring, class name, super classes, and so on) are no more accessible. In the same way, we can’t directly use the decorated object as a superclass.
For introspection, some of the meta attributes can be copied to the
decorated object, whether manually or using facility functions from the
functools
module (e.g. @wraps
or
update_wrapper
). However, not all properties or behaviours can be
copied in this way.
For subclassing, if the decorator is a class, as in the first
version, the class itself is accessible as the _cls
attribute, and
it is thus possible to subclass it, optionally redecorating it to also be a
flyweight. The process is however not elegant at all.
In the functional version, since the original class is hidden in the
closure, there is no way to subclass it.
If the flyweight classes need to be extended or analysed by introspection,
the modifying approaches are thus more suitable. The mixin approach works out of
the box, since we already deal with subclassing. The metaclass approach also
works fine, since the __metaclass__
is inherited, and the
metaclass constructor is called at class creation, for the class or its
subclasses.
Garbage collector
To be fully usable, the code snippets presented here should deal with
garbage collection, as discussed in the GoF (p. 200). Indeed, since the dictionary
in the flyweight factory keep
a reference to the created instance, it will never be collected, and the
destructor (__delete__
) method will never be called, until the end
of the main program execution, which can be memory expensive or prevent some
behaviour to take place (e.g. synchronization on deletion).
If the desired behaviour of the flyweight is to be deleted, one can use
python weak references, from the weakref
module. As
stated in the weakref documentation,
a weak reference to an object is not enough to keep the object alive: when
the only remaining references to a referent are weak references, garbage
collection is free to destroy the referent and reuse its memory for
something else
, which is exactly what is needed here.
Moreover, the
module provides a WeakValueDictionary
object, behaving like a
normal dict
, but storing only weak references to values. In the
previous code, replacing the instances dictionary by a
WeakValueDictionary
would allow the garbage collector to destroy
the cached instance when no other object is using it.
About usability
We have considered two distinct methods:
- delegation
- where the class is wrapped in an object providing the desired behaviour, like in the factory and the wrapping decorators;
- modification
- where the behaviours are added to the class:
From the final user point of view, considering code overweight, all approaches
are equivalents. Indeed, to make a class a flyweight, you just have to add a
metaclass, a parent mixin, or decorate the class (ignoring the two first
versions which just are a direct pattern implementation), and its use is
transparent (if using the versions that override __call__
or
__new__
.)
From the class metadata and subclassing point of view, modification is more suitable than delegation. Moreover, delegation creates a new object (instance or closure) for every flyweight class, whereas no overhead is introduced by the modification approaches.
However, delegation can be used with any callable object, whereas modification only applies to classes. It is therefore possible to use the decorator version to implement the memoize pattern, which keep the result of previous computations.
Finally, delegation and afterward modification are more flexible than inheritance or metaclass, which cannot be easily applied to existing classes.
The modifying decorator seems to be the more interesting approach in most of situations.