Featured image of post SP2 - Decorator & Function Decorator & Closure - Learn Design Pattern From Simple Things

SP2 - Decorator & Function Decorator & Closure - Learn Design Pattern From Simple Things

Having ice creams (popsicle, cone) and curious about how good it will be with extra flavor. For the most authentic experience, you should dip your ice creams in each flavor.

Decorator is a structural design pattern: šŸ–¤šŸ–¤šŸ–¤šŸ–¤šŸ¤

Don’t be confused! Decorator and Function Decorator have no relationship at all. However, In Python when people talk about decorator, mostly they are talking about Function Decorator.

What is Closure?

Closure is a technique of combining the callables (usually 2 functions) together prepare_recipe, updated_func that:

  • The variables layer_3, layer_2, layer_1 need to be stored for later usage in a specific scope. It will be passed to the outer/enclosing/wrapper function.

  • The outer/enclosing/wrapper function prepare_recipe stores variables. The inner/nested function will use the variables later.

  • The inner/nested function updated_func can access the variables thanks to the executed outer/enclosing/wrapper function.

Similar to Closure’s technique, we also have Function Decorator

What is Function Decorator?

Function Decorator is a technique of combining the callables (usually 2 functions) together prepare_recipe, updated_func that:

  • The origin/main function decorate_ice_cream is deprecated and needs to be updated a bit. It will be passed to the outer/enclosing/wrapper function.

  • The outer/enclosing/wrapper function prepare_recipe stores origin/main function. The inner/nested function will use the origin/main function later (Closure’s usage).

  • The inner/nested function updated_func will:

    • update behaviors around the origin/main function:
      • modify the input before giving the input to the origin/main function:
        • original input: PopsicleIceCream
        • modified input: Almond(Condensed(Chocolate( + PopsicleIceCream
      • modify the output of origin/main function before returning the final result:
        • original output: Almond(Condensed(Chocolate(PopsicleIceCream
        • modified output: Almond(Condensed(Chocolate(PopsicleIceCream + ))Milk)
    • then return the new version of the origin/main function.

What is Decorator?

Decorator help to add/update the object PopsicleIceCream dynamically by placing them inside the decorators ChocolateDecorator, CondensedMilkDecorator, AlmondDecorator.

It’s like a Function Decorator, but here it applies to Class Instance.

Decorator Diagram

Why use Closure?

Closure creates an environment to remember simple values layer_3, layer_2, layer_1, it’s like the class’s usage!

Do you know functools.partial? functools.partial function is also an example of Closure’s usage.

Question: Class is the alternative to Closure, then when should we use Closure in lieu of Class?

Answer: For simplicity, you should use Class for all cases. Closure is just a technique in theory, it’s probably only mentioned a lot in pointless interviews. I’m still bitter about that interview -_-

Why use Function Decorator?

It helps reduce code duplication by wrapping common code into a bundle def prepare_recipe, then simply adding @prepare_recipe above the concrete functions. This makes the function’s code shorter and easier to update/refactor.

Question: What are the alternatives if I’m too lazy to create the Function Decorator?

Answer: You can make a long code with duplication or a little better solution is to split the big function into small functions, the choice is yours! However, let’s reconsider using Function Decorator! Business before pleasure!

Why use Decorator?

Easily transform the original popsicle object into a almond_condensed_milk_chocolate_popsicle multiple features object dynamically, without affecting anything to the origin.

Question: I can easily add more features to an object without using Decorator, right?

Answer: Of course, but Decorator is lovely in its own way because it keeps the code clean by wrapping an popsicle object in new chocolate_popsicle objects and repeating that condensed_milk_chocolate_popsicle, almond_condensed_milk_chocolate_popsicle process endlessly without touching the old code!

When to use Closure?

Question: When should I use Closure?

Answer: When it’s necessary to create an instance in order to store values layer_3, layer_2, layer_1 and use decorate_ice_cream those values later

Input:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
popsicle_ice_cream = "PopsicleIceCream"
cone_ice_cream = "IceCreamCone"
print("[original ice creams] ")
print(popsicle_ice_cream)
print(cone_ice_cream)


# main function
def decorate_ice_cream(layer_3, layer_2, layer_1, ice_cream):
    magic_number = 9
    layer_2_a, layer_2_b = layer_2[:magic_number], layer_2[magic_number:]
    decorated_ice_cream = f"{layer_3}({layer_2_a}({layer_1}({ice_cream})){layer_2_b})"
    return decorated_ice_cream

Expected Output:

1
2
3
4
5
6
7
[original ice creams] 
PopsicleIceCream
IceCreamCone

[decorated ice creams] 
Almond(Condensed(Chocolate(PopsicleIceCream))Milk)
Almond(Condensed(Chocolate(IceCreamCone))Milk)

When to use Function Decorator?

Question: When should I use Function Decorator?

Answer: When the environment values, input values, output values surrounding the main/deprecated function needs to be updated before and after execution.

Input:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
popsicle_ice_cream = "PopsicleIceCream"
cone_ice_cream = "IceCreamCone"
print("[original ice creams] ")
print(popsicle_ice_cream)
print(cone_ice_cream)


# main function
def decorate_ice_cream(ice_cream):
    return ice_cream

Expected Output:

1
2
3
4
5
6
7
[original ice creams] 
PopsicleIceCream
IceCreamCone

[decorated ice creams] 
Almond(Condensed(Chocolate(PopsicleIceCream))Milk)
Almond(Condensed(Chocolate(IceCreamCone))Milk)

When to use Decorator?

Question: When should I use GoF Decorator?

Answer: When object properties need to be updated at runtime, but the Class structure remains origin.

Input:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class IceCream:
    def taste(self):
        return "IceCream"


class PopsicleIceCream(IceCream):
    def taste(self):
        return "PopsicleIceCream"


class ConeIceCream(IceCream):
    def taste(self):
        return "ConeIceCream"


popsicle = PopsicleIceCream()
cone = ConeIceCream()
print("[original ice creams] ")
print(popsicle.taste())
print(cone.taste())

Expected Output:

1
2
3
4
5
6
7
[original ice creams] 
PopsicleIceCream
ConeIceCream

[decorated ice creams] 
Almond(Condensed(Chocolate(PopsicleIceCream))Milk)
Almond(Condensed(Chocolate(ConeIceCream))Milk)

How to implement Closure?

Non-Closure Implementation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class Recipe:
    def __init__(self, func, layer_3, layer_2, layer_1):
        self.func = func
        self.layer_3 = layer_3
        self.layer_2 = layer_2
        self.layer_1 = layer_1

    def decorate_ice_cream(self, ice_cream):
        return self.func(self.layer_3, self.layer_2, self.layer_1, ice_cream)


if __name__ == "__main__":
    recipe = Recipe(decorate_ice_cream, "Almond", "CondensedMilk", "Chocolate")
    almond_condensed_milk_chocolate_popsicle = recipe.decorate_ice_cream(popsicle_ice_cream)
    almond_condensed_milk_chocolate_cone = recipe.decorate_ice_cream(cone_ice_cream)
    print("\n[decorated ice creams] ")
    print(almond_condensed_milk_chocolate_popsicle)
    print(almond_condensed_milk_chocolate_cone)

Closure Implementation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# closure
def prepare_recipe(func, layer_3, layer_2, layer_1):
    def updated_func(ice_cream):
        return func(layer_3, layer_2, layer_1, ice_cream)

    return updated_func


if __name__ == "__main__":
    # closure's outcome: updated function
    recipe = prepare_recipe(decorate_ice_cream, "Almond", "CondensedMilk", "Chocolate")
    almond_condensed_milk_chocolate_popsicle = recipe(popsicle_ice_cream)
    almond_condensed_milk_chocolate_cone = recipe(cone_ice_cream)
    print("\n[decorated ice creams] ")
    print(almond_condensed_milk_chocolate_popsicle)
    print(almond_condensed_milk_chocolate_cone)

Source Code: Closure

How to implement Function Decorator?

Non-Function Decorator Implementation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# main function
def decorate_ice_cream(ice_cream):
    magic_number = 9
    layer_3, layer_2, layer_1 = "Almond", "CondensedMilk", "Chocolate"
    layer_2_a, layer_2_b = layer_2[:magic_number], layer_2[magic_number:]
    front_ice_cream = f"{layer_3}({layer_2_a}({layer_1}("
    back_ice_cream = f")){layer_2_b})"
    half_decorated_ice_cream = front_ice_cream + ice_cream
    decorated_ice_cream = half_decorated_ice_cream + back_ice_cream
    return decorated_ice_cream


if __name__ == "__main__":
    almond_condensed_milk_chocolate_popsicle = decorate_ice_cream(popsicle_ice_cream)
    almond_condensed_milk_chocolate_cone = decorate_ice_cream(cone_ice_cream)
    print("\n[decorated ice creams] ")
    print(almond_condensed_milk_chocolate_popsicle)
    print(almond_condensed_milk_chocolate_cone)

Function Decorator Implementation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# decorator
def prepare_recipe(func):
    def updated_func(ice_cream):
        magic_number = 9
        layer_3, layer_2, layer_1 = "Almond", "CondensedMilk", "Chocolate"
        layer_2_a, layer_2_b = layer_2[:magic_number], layer_2[magic_number:]
        front_ice_cream = f"{layer_3}({layer_2_a}({layer_1}("
        back_ice_cream = f")){layer_2_b})"
        half_decorated_ice_cream = func(front_ice_cream + ice_cream)
        decorated_ice_cream = half_decorated_ice_cream + back_ice_cream
        return decorated_ice_cream

    return updated_func


@prepare_recipe
# main function
def decorate_ice_cream(ice_cream):
    return ice_cream


if __name__ == "__main__":
    almond_condensed_milk_chocolate_popsicle = decorate_ice_cream(popsicle_ice_cream)
    almond_condensed_milk_chocolate_cone = decorate_ice_cream(cone_ice_cream)
    print("\n[decorated ice creams] ")
    print(almond_condensed_milk_chocolate_popsicle)
    print(almond_condensed_milk_chocolate_cone)

Source Code: Function Decorator

How to implement Function Decorator With Params?

Theory about Function Decorator without params:

A) If we have:

1
2
def decorate_ice_cream(ice_cream):
    return ice_cream

B) We can create decorator like this:

1
2
3
4
def prepare_recipe(func):
    def updated_func(ice_cream):
        return ... func(...) ...
    return updated_func

C) Then apply decorator to the main function by:

1
2
3
@prepare_recipe
def decorate_ice_cream(ice_cream):
    return ice_cream

or

1
decorate_ice_cream = prepare_recipe(decorate_ice_cream)

Theory about Function Decorator with params:

Those params need to be stored. It’s time for Closure to show off!

  • prepare_recipe = prepare_recipe_with_params(layer_3, layer_2, layer_1)
  • Function Decorator With Params = Closure + Function Decorator Without Params

A) If we have:

1
2
def decorate_ice_cream(ice_cream):
    return ice_cream

B) We can create decorator like this:

closure will create environment to store layer_3, layer_2, layer_1

1
2
3
4
5
6
7
8
# closure
def prepare_recipe_with_params(layer_3, layer_2, layer_1):
    # decorator
    def prepare_recipe(func):
        def updated_func(ice_cream):
            return ... func(...) ...
        return updated_func
    return prepare_recipe

C) Then apply decorator to the main function by:

1
2
3
@prepare_recipe(layer_3, layer_2, layer_1)
def decorate_ice_cream(ice_cream):
    return ice_cream

or

1
2
3
# closure's outcome:
prepare_recipe = prepare_recipe_with_params(layer_3, layer_2, layer_1)
decorate_ice_cream = prepare_recipe(decorate_ice_cream)

Function Decorator With Params Implementation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# decorator with params
def prepare_recipe_with_params(layer_3, layer_2, layer_1):
    def prepare_recipe(func):
        def updated_func(ice_cream):
            magic_number = 9
            layer_2_a, layer_2_b = layer_2[:magic_number], layer_2[magic_number:]
            front_ice_cream = f"{layer_3}({layer_2_a}({layer_1}("
            back_ice_cream = f")){layer_2_b})"
            half_decorated_ice_cream = func(front_ice_cream + ice_cream)
            decorated_ice_cream = half_decorated_ice_cream + back_ice_cream
            return decorated_ice_cream

        return updated_func

    return prepare_recipe


@prepare_recipe_with_params("Almond", "CondensedMilk", "Chocolate")
# main function
def decorate_ice_cream(ice_cream):
    return ice_cream


if __name__ == "__main__":
    almond_condensed_milk_chocolate_popsicle = decorate_ice_cream(popsicle_ice_cream)
    almond_condensed_milk_chocolate_cone = decorate_ice_cream(cone_ice_cream)
    print("\n[decorated ice creams] ")
    print(almond_condensed_milk_chocolate_popsicle)
    print(almond_condensed_milk_chocolate_cone)

Source Code: Function Decorator With Params Manually

Source Code: Function Decorator With Params

How to implement Decorator?

Non-Decorator Implementation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def generate(ice_cream, flavor):
    old_taste = ice_cream.taste()
    if "Chocolate" == flavor:

        def _taste_chocolate():
            return f"Chocolate({old_taste})"

        ice_cream.taste = _taste_chocolate
    elif "CondensedMilk" == flavor:

        def _taste_condensed_mild():
            return f"Condensed({old_taste})Milk"

        ice_cream.taste = _taste_condensed_mild
    elif "Almond" == flavor:

        def _taste_almond():
            return f"Almond({old_taste})"

        ice_cream.taste = _taste_almond
    return ice_cream


if __name__ == "__main__":
    chocolate_popsicle = generate(popsicle, "Chocolate")
    chocolate_cone = generate(cone, "Chocolate")
    condensed_milk_chocolate_popsicle = generate(chocolate_popsicle, "CondensedMilk")
    condensed_milk_chocolate_cone = generate(chocolate_cone, "CondensedMilk")
    almond_condensed_milk_chocolate_popsicle = generate(
        condensed_milk_chocolate_popsicle, "Almond"
    )
    almond_condensed_milk_chocolate_cone = generate(
        condensed_milk_chocolate_cone, "Almond"
    )
    print("\n[decorated ice creams] ")
    print(almond_condensed_milk_chocolate_popsicle.taste())
    print(almond_condensed_milk_chocolate_cone.taste())

Decorator Implementation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class Decorator(IceCream):
    _component = None

    def __init__(self, component):
        print(f"{type(self)} is decorating the {type(component)}")
        self._component = component

    @property
    def component(self):
        return self._component

    def taste(self):
        return self._component.taste()


class ChocolateDecorator(Decorator):
    def taste(self):
        return f"Chocolate({self.component.taste()})"


class CondensedMilkDecorator(Decorator):
    def taste(self):
        return f"Condensed({self.component.taste()})Milk"


class AlmondDecorator(Decorator):
    def taste(self):
        return f"Almond({self.component.taste()})"


if __name__ == "__main__":
    chocolate_popsicle = ChocolateDecorator(popsicle)
    chocolate_cone = ChocolateDecorator(cone)
    condensed_milk_chocolate_popsicle = CondensedMilkDecorator(chocolate_popsicle)
    condensed_milk_chocolate_cone = CondensedMilkDecorator(chocolate_cone)
    almond_condensed_milk_chocolate_popsicle = AlmondDecorator(
        condensed_milk_chocolate_popsicle
    )
    almond_condensed_milk_chocolate_cone = AlmondDecorator(
        condensed_milk_chocolate_cone
    )
    print("\n[decorated ice creams] ")
    print(almond_condensed_milk_chocolate_popsicle.taste())
    print(almond_condensed_milk_chocolate_cone.taste())

Source Code: Decorator

Made with the laziness šŸ¦„
by a busy guy