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.

Code for the strict GoF version (strictgof.py)
    1 class Spam(object):
    2     def __init__(self, a, b):
    3         self.a = a
    4         self.b = b
    5 
    6 class SpamFactory(object):
    7     def  __init__(self):
    8         self.__instances = dict()
    9 
   10     def get_instance(self, a, b):
   11         if (a, b) not in self.__instances:
   12             self.__instances[(a, b)] = Spam(a, b)
   13         return self.__instances[(a, b)]
   14 
   15 
   16 class Egg(object):
   17     def __init__(self, x, y):
   18         self.x = x
   19         self.y = y
   20 
   21 class EggFactory(object):
   22     def __init__(self):
   23         self.__instances = dict()
   24 
   25     def get_instance(self, x, y):
   26         if (x, y) not in self.__instances:
   27             self.__instances[(x, y)] = Egg(x, y)
   28         return self.__instances[(x, y)]
   29 
   30 #----------------------------------------------------------
   31 
   32 spamFactory = SpamFactory()
   33 eggFactory = EggFactory()
   34 
   35 assert spamFactory.get_instance(1, 2) is spamFactory.get_instance(1, 2)
   36 assert eggFactory.get_instance('a', 'b') is eggFactory.get_instance('a', 'b')
   37 assert spamFactory.get_instance(1, 2) is not eggFactory.get_instance(1, 2)

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.

Code for the GoF version (gof.py)
    1 class FlyweightFactory(object):
    2     def __init__(self, cls):
    3         self._cls = cls
    4         self._instances = dict()
    5 
    6     def get_instance(self, *args, **kargs):
    7         return self._instances.setdefault(
    8                                 (args, tuple(kargs.items())),
    9                                 self._cls(*args, **kargs))
   10 
   11 
   12 #----------------------------------------------------------
   13 class Spam(object):
   14     def __init__(self, a, b):
   15         self.a = a
   16         self.b = b
   17 
   18 
   19 class Egg(object):
   20     def __init__(self, x, y):
   21         self.x = x
   22         self.y = y
   23 
   24 
   25 SpamFactory = FlyweightFactory(Spam)
   26 EggFactory = FlyweightFactory(Egg)
   27 
   28 assert SpamFactory.get_instance(1, 2) is SpamFactory.get_instance(1, 2)
   29 assert EggFactory.get_instance('a', 'b') is EggFactory.get_instance('a', 'b')
   30 assert SpamFactory.get_instance(1, 2) is not EggFactory.get_instance(1, 2)
   31 
   32 # Subclassing a flyweight class
   33 class SubSpam(Spam):
   34     pass
   35 
   36 SubSpamFactory = FlyweightFactory(SubSpam)
   37 
   38 assert SubSpamFactory.get_instance(1,2) is SubSpamFactory.get_instance(1,2)
   39 assert SpamFactory.get_instance(1,2) is not SubSpamFactory.get_instance(1,2)

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.

Code for the wrapping decorator version (decowrap.py)
    1 class flyweight(object):
    2     def __init__(self, cls):
    3         self._cls = cls
    4         self._instances = dict()
    5 
    6     def __call__(self, *args, **kargs):
    7         return self._instances.setdefault(
    8                                     (args, tuple(kargs.items())),
    9                                     self._cls(*args, **kargs))
   10 
   11 
   12 #----------------------------------------------------------
   13 @flyweight
   14 class Spam(object):
   15     def __init__(self, a, b):
   16         self.a = a
   17         self.b = b
   18 
   19 
   20 @flyweight
   21 class Egg(object):
   22     def __init__(self, x, y):
   23         self.x = x
   24         self.y = y
   25 
   26 
   27 assert Spam(1, 2) is Spam(1, 2)
   28 assert Egg('a', 'b') is Egg('a', 'b')
   29 assert Spam(1, 2) is not Egg(1, 2)
   30 
   31 # Subclassing a flyweight class
   32 @flyweight
   33 class SubSpam(Spam._cls):
   34     pass
   35 
   36 assert SubSpam(1,2) is SubSpam(1,2)
   37 assert Spam(1,2) is not SubSpam(1,2)

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).

Code for the functional wrapping decorator version (functdecowrap.py)
    1 def flyweight(cls):
    2     instances = dict()
    3     return lambda *args, **kargs: instances.setdefault(
    4                                             (args, tuple(kargs.items())),
    5                                             cls(*args, **kargs))
    6 
    7 
    8 #----------------------------------------------------------
    9 @flyweight
   10 class Spam(object):
   11     def __init__(self, a, b):
   12         self.a = a
   13         self.b = b
   14 
   15 
   16 @flyweight
   17 class Egg(object):
   18     def __init__(self, x, y):
   19         self.x = x
   20         self.y = y
   21 
   22 
   23 assert Spam(1, 2) is Spam(1, 2)
   24 assert Egg('a', 'b') is Egg('a', 'b')
   25 assert Spam(1, 2) is not Egg(1, 2)
   26 
   27 # No way to subclass

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.

Code for the GoF mixin version (gofmixin.py)
    1 class FlyweightMixin(object):
    2 
    3     _instances = dict()
    4 
    5     @classmethod
    6     def get_instance(cls, *args, **kargs):
    7         return cls._instances.setdefault(
    8                                 (cls, args, tuple(kargs.items())), 
    9                                 cls(*args, **kargs))
   10 
   11 
   12 #----------------------------------------------------------
   13 class Spam(FlyweightMixin):
   14 
   15     def __init__(self, a, b):
   16         self.a = a
   17         self.b = b
   18 
   19 
   20 class Egg(FlyweightMixin):
   21 
   22     def __init__(self, x, y):
   23         self.x = x
   24         self.y = y
   25 
   26 
   27 assert Spam.get_instance(1, 2) is Spam.get_instance(1, 2)
   28 assert Egg.get_instance('a', 'b') is Egg.get_instance('a', 'b')
   29 assert Spam.get_instance(1, 2) is not Egg.get_instance(1, 2)
   30 
   31 # Subclassing a flyweight class
   32 class SubSpam(Spam):
   33     pass
   34 
   35 assert SubSpam.get_instance(1,2) is SubSpam.get_instance(1,2)
   36 assert Spam.get_instance(1,2) is not SubSpam.get_instance(1,2)

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.

Code for mixin the version (mixinnew.py)
    1 class FlyweightMixin(object):
    2     _instances = dict()
    3     def __init__(self, *args, **kargs):
    4         raise NotImplementedException
    5 
    6     def __new__(cls, *args, **kargs):
    7         return cls._instances.setdefault(
    8                     (cls, args, tuple(kargs.items())),
    9                     super(type(cls), cls).__new__(cls, *args, **kargs))
   10 
   11 
   12 #----------------------------------------------------------
   13 class Spam(FlyweightMixin):
   14 
   15     def __init__(self, a, b):
   16         self.a = a
   17         self.b = b
   18 
   19 
   20 class Egg(FlyweightMixin):
   21 
   22     def __init__(self, x, y):
   23         self.x = x
   24         self.y = y
   25 
   26 
   27 assert Spam(1, 2) is Spam(1, 2)
   28 assert Egg('a', 'b') is Egg('a', 'b')
   29 assert Spam(1, 2) is not Egg(1, 2)
   30 
   31 # Subclassing a flyweight class
   32 class SubSpam(Spam):
   33     pass
   34 
   35 assert SubSpam(1,2) is SubSpam(1,2)
   36 assert Spam(1,2) is not SubSpam(1,2)

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.

Code for the modifying decorator version (decomod.py)
    1 @classmethod
    2 def _get_instance(cls, *args, **kargs):
    3     return cls.__instances.setdefault(
    4                                 (args, tuple(kargs.items())),
    5                                 super(type(cls), cls).__new__(*args, **kargs))
    6 
    7 
    8 def flyweight(decoree):
    9     decoree.__instances = dict()
   10     decoree.__new__ = _get_instance
   11     return decoree
   12 
   13 
   14 #----------------------------------------------------------
   15 @flyweight
   16 class Spam(object):
   17     def __init__(self, a, b):
   18         self.a = a
   19         self.b = b
   20 
   21 
   22 @flyweight
   23 class Egg(object):
   24     def __init__(self, x, y):
   25         self.x = x
   26         self.y = y
   27 
   28 
   29 assert Spam(1, 2) is Spam(1, 2)
   30 assert Egg('a', 'b') is Egg('a', 'b')
   31 assert Spam(1, 2) is not Egg(1, 2)
   32 
   33 # Subclassing a flyweight class
   34 class SubSpam(Spam):
   35     pass
   36 
   37 assert SubSpam(1,2) is SubSpam(1,2)
   38 assert Spam(1,2) is not SubSpam(1,2)

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).

Code for the metaclass version (meta.py)
    1 class MetaFlyweight(type):
    2     def __init__(cls, *args, **kargs):
    3         type.__init__(cls, *args, **kargs)
    4         cls.__instances = dict()
    5         cls.__new__ = cls._get_instance
    6 
    7     def _get_instance(cls, *args, **kargs):
    8         return cls.__instances.setdefault(
    9                                     (args, tuple(kargs.items())),
   10                                     super(cls, cls).__new__(*args, **kargs))
   11 
   12 
   13 #----------------------------------------------------------
   14 class Spam(object):
   15     __metaclass__ = MetaFlyweight
   16 
   17     def __init__(self, a, b):
   18         self.a = a
   19         self.b = b
   20 
   21 
   22 class Egg(object):
   23     __metaclass__ = MetaFlyweight
   24 
   25     def __init__(self, x, y):
   26         self.x = x
   27         self.y = y
   28 
   29 
   30 assert Spam(1, 2) is Spam(1, 2)
   31 assert Egg('a', 'b') is Egg('a', 'b')
   32 assert Spam(1, 2) is not Egg(1, 2)
   33 
   34 # Subclassing a flyweight class
   35 class SubSpam(Spam):
   36     pass
   37 
   38 assert SubSpam(1,2) is SubSpam(1,2)
   39 assert Spam(1,2) is not SubSpam(1,2)

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.

Code for the functional metaclass version (functmeta.py)
    1 @classmethod
    2 def _get_instance(cls, *args, **kargs):
    3     return cls.__instances.setdefault(
    4                                 (args, tuple(kargs.items())),
    5                                 super(type(cls), cls).__new__(*args, **kargs))
    6 
    7 def metaflyweight(name, parents, attrs):
    8     cls = type(name, parents, attrs)
    9     cls.__instances = dict()
   10     cls.__new__ = _get_instance
   11     return cls
   12 
   13 
   14 #----------------------------------------------------------
   15 class Spam(object):
   16     __metaclass__ = metaflyweight
   17 
   18     def __init__(self, a, b):
   19         self.a = a
   20         self.b = b
   21 
   22 
   23 class Egg(object):
   24     __metaclass__ = metaflyweight
   25 
   26     def __init__(self, x, y):
   27         self.x = x
   28         self.y = y
   29 
   30 
   31 assert Spam(1, 2) is Spam(1, 2)
   32 assert Egg('a', 'b') is Egg('a', 'b')
   33 assert Spam(1, 2) is not Egg(1, 2)
   34 
   35 # Subclassing a flyweight class
   36 class SubSpam(Spam):
   37     pass
   38 
   39 assert SubSpam(1,2) is SubSpam(1,2)
   40 assert Spam(1,2) is not SubSpam(1,2)

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!

Code for the pure functional metaclass version (purefunctmeta.py)
    1 metaflyweight = lambda name, parents, attrs: type(
    2         name,
    3         parents,
    4         dict(attrs.items() + [
    5             ('__instances', dict()),
    6             ('__new__', classmethod(
    7                 lambda cls, *args, **kargs: cls.__instances.setdefault(
    8                                 tuple(args),
    9                                 super(type(cls), cls).__new__(*args, **kargs))
   10                 )
   11             )
   12         ])
   13     )
   14 
   15 
   16 #----------------------------------------------------------
   17 class Spam(object):
   18     __metaclass__ = metaflyweight
   19 
   20     def __init__(self, a, b):
   21         self.a = a
   22         self.b = b
   23 
   24 
   25 class Egg(object):
   26     __metaclass__ = metaflyweight
   27 
   28     def __init__(self, x, y):
   29         self.x = x
   30         self.y = y
   31 
   32 
   33 assert Spam(1, 2) is Spam(1, 2)
   34 assert Egg('a', 'b') is Egg('a', 'b')
   35 assert Spam(1, 2) is not Egg(1, 2)
   36 
   37 # Subclassing a flyweight class
   38 class SubSpam(Spam):
   39     pass
   40 
   41 assert SubSpam(1,2) is SubSpam(1,2)
   42 assert Spam(1,2) is not SubSpam(1,2)
+

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:
  • by modifying it afterward using a decorator;
  • by modifying if at creation time using a metaclass;
  • by using a mixin and inheritance.

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.