Change the Flyweight pattern to Pythonic (?) (1)

A memo that interprets the article taught by the person next to the side.

Flyweight pattern faithful to GoF

One factory is defined for one target. The code below is pretty much the same as the original article, with changes to give named arguments.

python


class Hoge(object):
    def __init__(self, args1, kwargs1='test1'):
        self.args1 = args1
        self.kwargs1 = kwargs1

class HogeFactory(object):
    def  __init__(self):
        self._instances = {}

    def get_instance(self, args1, kwargs1):
        if ((args1), (kwargs1)) not in self._instances:
            self._instances[((args1), (kwargs1))] = Hoge(args1, kwargs1=kwargs1)
        return self._instances[((args1), (kwargs1))]


class Piyo(object):
    def __init__(self, args1, kwargs1, kwargs2):
        self.args1 = args1
        self.kwargs1 = kwargs1
        self.kwargs2 = kwargs2

class PiyoFactory(object):
    def __init__(self):
        self._instances = {}

    def get_instance(self, args1, kwargs1='test1', kwargs2='test2'):
        if ((args1), (kwargs1, kwargs2)) not in self._instances:
            self._instances[((args1), (kwargs1, kwargs2))] = Piyo(args1, kwargs1=kwargs1, kwargs2=kwargs2)
        return self._instances[((args1), (kwargs1, kwargs2))]

hogeFactory = HogeFactory()
piyoFactory = PiyoFactory()

assert hogeFactory.get_instance(1, kwargs1=2) is hogeFactory.get_instance(1, kwargs1=2)
assert hogeFactory.get_instance(1, kwargs1=2) is not hogeFactory.get_instance(1, kwargs1=3)
assert piyoFactory.get_instance('a', kwargs1='b', kwargs2='c') is piyoFactory.get_instance('a', kwargs1='b', kwargs2='c')
assert piyoFactory.get_instance('a', kwargs1='b', kwargs2='c') is not piyoFactory.get_instance('a', kwargs1='b', kwargs2='d')

Generalization of Factory with variadic arguments

The reason why one factory was needed for one target was that each target's constructor had a different signature = get_instance method had a different signature. You can use Python's variadic arguments to absorb differences in signatures.

python


class FlyweightFactory(object):
    def __init__(self, cls):
        self._instances = {}
        self._cls = cls

    def get_instance(self, *args, **kwargs):
        return self._instances.setdefault((args, tuple(kwargs.items())), self._cls(*args, **kwargs))

class Hoge(object):
    def __init__(self, args1, kwargs1='test1'):
        self.args1 = args1
        self.kwargs1 = kwargs1

class Piyo(object):
    def __init__(self, args1, kwargs1, kwargs2):
        self.args1 = args1
        self.kwargs1 = kwargs1
        self.kwargs2 = kwargs2

hogeFactory = FlyweightFactory(Hoge)
piyoFactory = FlyweightFactory(Piyo)

assert hogeFactory.get_instance(1, kwargs1=2) is hogeFactory.get_instance(1, kwargs1=2)
assert hogeFactory.get_instance(1, kwargs1=2) is not hogeFactory.get_instance(1, kwargs1=3)
assert piyoFactory.get_instance('a', kwargs1='b', kwargs2='c') is piyoFactory.get_instance('a', kwargs1='b', kwargs2='c')
assert piyoFactory.get_instance('a', kwargs1='b', kwargs2='c') is not piyoFactory.get_instance('a', kwargs1='b', kwargs2='d')

Factory decorator (class)

You can write it even simpler by decorating the above FlyweightFactory.

python


class Flyweight(object):
    def __init__(self, cls):
        self._instances = {}
        self._cls = cls

    def __call__(self, *args, **kwargs):
        return self._instances.setdefault((args, tuple(kwargs.items())), self._cls(*args, **kwargs))

@Flyweight
class Hoge(object):
    def __init__(self, args1, kwargs1='test1'):
        self.args1 = args1
        self.kwargs1 = kwargs1

@Flyweight
class Piyo(object):
    def __init__(self, args1, kwargs1, kwargs2):
        self.args1 = args1
        self.kwargs1 = kwargs1
        self.kwargs2 = kwargs2

assert Hoge(1, kwargs1=2) is Hoge(1, kwargs1=2)
assert Hoge(1, kwargs1=2) is not Hoge(1, kwargs1=3)
assert Piyo('a', kwargs1='b', kwargs2='c') is Piyo('a', kwargs1='b', kwargs2='c')
assert Piyo('a', kwargs1='b', kwargs2='c') is not Piyo('a', kwargs1='b', kwargs2='d')

The decorator is called by taking the attached target as an argument. The code equivalent to this process can be written as follows without using a decorator. Instead of calling get_instance, there is a difference in reading the Flyweight instance as a function, but you can see that it is not so different from the previous snippet.

(ref. http://docs.python.jp/3.4/reference/datamodel.html#object.call)

python


class Flyweight(object):
    def __init__(self, cls):
        self._instances = {}
        self._cls = cls

    def __call__(self, *args, **kwargs):
        return self._instances.setdefault((args, tuple(kwargs.items())), self._cls(*args, **kwargs))

class Hoge(object):
    def __init__(self, args1, kwargs1='test1'):
        self.args1 = args1
        self.kwargs1 = kwargs1

class Piyo(object):
    def __init__(self, args1, kwargs1, kwargs2):
        self.args1 = args1
        self.kwargs1 = kwargs1
        self.kwargs2 = kwargs2

# Hoge,Take Piyo as an argument,Create a Flyweight instance.
Hoge = Flyweight(Hoge)
Piyo = Flyweight(Piyo)

assert Hoge(1, kwargs1=2) is Hoge(1, kwargs1=2)
assert Hoge(1, kwargs1=2) is not Hoge(1, kwargs1=3)
assert Piyo('a', kwargs1='b', kwargs2='c') is Piyo('a', kwargs1='b', kwargs2='c')
assert Piyo('a', kwargs1='b', kwargs2='c') is not Piyo('a', kwargs1='b', kwargs2='d')

Factory decorator (closure)

Furthermore, the implementation using the following closures does not have to be a class as long as it is callable.

python


def flyweight(cls):
    instances = {}
    return lambda *args, **kwargs: instances.setdefault((args, tuple(kwargs.items())), cls(*args, **kwargs))

@flyweight
class Hoge(object):
    def __init__(self, args1, kwargs1='test1'):
        self.args1 = args1
        self.kwargs1 = kwargs1

@flyweight
class Piyo(object):
    def __init__(self, args1, kwargs1, kwargs2):
        self.args1 = args1
        self.kwargs1 = kwargs1
        self.kwargs2 = kwargs2

assert Hoge(1, kwargs1=2) is Hoge(1, kwargs1=2)
assert Hoge(1, kwargs1=2) is not Hoge(1, kwargs1=3)
assert Piyo('a', kwargs1='b', kwargs2='c') is Piyo('a', kwargs1='b', kwargs2='c')
assert Piyo('a', kwargs1='b', kwargs2='c') is not Piyo('a', kwargs1='b', kwargs2='d')

The equivalent code without a decorator looks like this:

python


def flyweight(cls):
    instances = {}
    return lambda *args, **kwargs: instances.setdefault((args, tuple(kwargs.items())), cls(*args, **kwargs))

class Hoge(object):
    def __init__(self, args1, kwargs1='test1'):
        self.args1 = args1
        self.kwargs1 = kwargs1

class Piyo(object):
    def __init__(self, args1, kwargs1, kwargs2):
        self.args1 = args1
        self.kwargs1 = kwargs1
        self.kwargs2 = kwargs2

Hoge = flyweight(Hoge)
Piyo = flyweight(Piyo)

assert Hoge(1, kwargs1=2) is Hoge(1, kwargs1=2)
assert Hoge(1, kwargs1=2) is not Hoge(1, kwargs1=3)
assert Piyo('a', kwargs1='b', kwargs2='c') is Piyo('a', kwargs1='b', kwargs2='c')
assert Piyo('a', kwargs1='b', kwargs2='c') is not Piyo('a', kwargs1='b', kwargs2='d')

This method is very smart, but it has the disadvantage that you can't create a subclass of the Hoge / Piyo class. This is because the Hoge / Piyo is no longer a class, but a function object, as you can see in the code that doesn't use decorators. Therefore, the following code will result in an error.

python


def flyweight(cls):
    instances = {}
    return lambda *args, **kwargs: instances.setdefault((args, tuple(kwargs.items())), cls(*args, **kwargs))

@flyweight
class Hoge(object):
    def __init__(self, args1, kwargs1='test1'):
        self.args1 = args1
        self.kwargs1 = kwargs1

@flyweight
class Piyo(object):
    def __init__(self, args1, kwargs1, kwargs2):
        self.args1 = args1
        self.kwargs1 = kwargs1
        self.kwargs2 = kwargs2

#Hoge is no longer a class and cannot be inherited
class Hoge2(Hoge):
    def __init__(self, args1, kwargs1='test1'):
        super().__init__(args1, kwargs1=kwargs1)

In the sense that the decorator makes Hoge / Piyo no longer a class, it doesn't change when the decorator is represented by a class, but there is an instance of the Flyweight class that replaces the Hoge / Piyo class. It is the miso that holds the original class. Therefore, it can be subclassed with the following code.

python


class Flyweight(object):
    def __init__(self, cls):
        self._instances = {}
        self._cls = cls

    def __call__(self, *args, **kwargs):
        return self._instances.setdefault((args, tuple(kwargs.items())), self._cls(*args, **kwargs))

@Flyweight
class Hoge(object):
    def __init__(self, args1, kwargs1='test1'):
        self.args1 = args1
        self.kwargs1 = kwargs1

@Flyweight
class Piyo(object):
    def __init__(self, args1, kwargs1, kwargs2):
        self.args1 = args1
        self.kwargs1 = kwargs1
        self.kwargs2 = kwargs2

# Hoge(The entity is a Flyweight instance)Is an instance variable that holds the original class_Have cls
class Hoge2(Hoge._cls):
    def __init__(self, args1, kwargs1='test1'):
        super().__init__(args1, kwargs1=kwargs1)

Recommended Posts

Change the Flyweight pattern to Pythonic (?) (3)
Change the Flyweight pattern to Pythonic (?) (2)
Change the Flyweight pattern to Pythonic (?) (1)
[Python] Change the alphabet to numbers
Learn the design pattern "Flyweight" in Python
Script to change the description of fasta
[Python] How to change the date format (display format)
Change the decimal point of logging from, to.
Flyweight pattern in Java
The road to Pythonista
About the Visitor pattern
The road to Djangoist
[Python] Change the Cache-Control of the object uploaded to Cloud Storage
Change the Amazon Linux locale to Japan using Ansible's lineinfile
[python] Change the image file name to a serial number
Change the standard output destination to a file in Python
An introduction to object orientation-let's change the internal state of an object
I want to change the Japanese flag to the Palau flag with Numpy
Change the volume of Pepper according to the surrounding environment (sound)
Change the message displayed when logging in to Raspberry Pi
Change the installation destination when --user is added to pip
How to use the generator
Dot according to the image
Change the theme of Jupyter
Change the style of matplotlib
How to change Python version
How to use the decorator
How to increase the axis
How to start the program
How to change the log level of Azure SDK for Python
How to change the color of just the button pressed in Tkinter
Change the Y-axis scale of Matplotlib to exponential notation (10 Nth power notation)
Feel free to change the label of the legend in Seaborn in python
[Go] Create a CLI command to change the extension of the image
Change the bash prompt to a simple color for easy viewing
I summarized how to change the boot parameters of GRUB and GRUB2
Change the active version in Pyenv from anaconda to plain Python