본문 바로가기
Mastering Python Design Patterns

The Decorator Pattern

by 자동매매 2023. 3. 22.

5 The Decorator Pattern

As we saw in the previous chapter, using an adapter, a first structural design pattern, you can adapt an object implementing a given interface to implement another interface. This is called interface adaptation and includes the kinds of patterns that encourage composition over inheritance, and it could bring benefits when you have to maintain a large codebase.

A second interesting structural pattern to learn about is the decorator pattern, which allows a programmer to add responsibilities to an object dynamically, and in a transparent manner (without affecting other objects).

There is another reason why this pattern is interesting to us, as you will see in a minute.

As Python developers, we can write decorators in a Pythonic way (meaning using the language's features), thanks to the built-in decorator feature (https:/ / docs.python.org/3/ reference/compound_stmts.html#function). What exactly is this feature? A Python decorator is a callable (function, method, or class) that gets a function object func_in as input, and returns another function object func_out. It is a commonly used technique for extending the behavior of a function, method, or class.

But, this feature should not be completely new to you. We have already seen how to use the built-in property decorator that makes a method appear as a variable in both Chapter 1,

The Factory Pattern, and Chapter 2, *The Builder Pattern. And there are several other useful built-in decorators in Python. In the Implementation section of this chapter, we will learn how to implement and use our own decorators.

Note that there is no one-to-one relationship between the decorator pattern and Python's decorator feature. Python decorators can actually do much more than the decorator pattern. One of the things they can be used for is to implement the decorator pattern

(j.mp/moinpydec).

The Decorator Pattern Chapter 63*

In this chapter, we will discuss:

  • Real-world examples
    • Use cases
      • Implementation

Real-world examples

The decorator pattern is generally used for extending the functionality of an object. In everyday life, examples of such extensions are: adding a silencer to a gun, using different camera lenses, and so on.

In the Django Framework, which uses decorators a lot, we have the View decorators which can be used for (j.mp/djangodec) the following:

  • Restricting access to views based on the HTTP request
    • Controlling the caching behavior on specific views
      • Controlling compression on a per-view basis
        • Controlling caching based on specific HTTP request headers

Both the Pyramid Framework and the Zope application server also use decorators to achieve various goals:

  • Registering a function as an event subscriber
    • Protecting a method with a specific permission
      • Implementing the adapter pattern

Use cases

The decorator pattern shines when used for implementing cross-cutting concerns (j.mp/wikicrosscut). Examples of cross-cutting concerns are as follows:

  • Data validation
    • Caching
      • Logging
  • Monitoring
    • Debugging
      • Business rules
        • Encryption

In general, all parts of an application that are generic and can be applied to many other parts of it are considered to be cross-cutting concerns.

Another popular example of using the decorator pattern is graphical user interface (GUI) toolkits. In a GUI toolkit, we want to be able to add features such as borders, shadows, colors, and scrolling to individual components/widgets.

Implementation

Python decorators are generic and very powerful. You can find many examples of how they can be used at the decorator library of python.org (j.mp/pydeclib). In this section, we will see how we can implement a memoization decorator (j.mp/memoi). All recursive functions can benefit from memoization, so let's try a function number_sum() that returns

the sum of the first n numbers. Note that this function is already available in the math module as fsum(), but let's pretend it is not.

First, let's look at the naive implementation (the number_sum_naive.py file):

def number_sum(n):

'''Returns the sum of the first n numbers''' assert(n >= 0), 'n must be >= 0'

if n == 0:

return 0

else:

return n + number_sum(n-1)

if __name__ == '__main__':

from timeit import Timer

t = Timer('number_sum(30)', 'from __main__ import number_sum') print('Time: ', t.timeit())

A sample execution of this example shows how slow this implementation is. It takes 15 seconds to calculate the sum of the first 30 numbers. We get the following output when executing the python number_sum_naive.py command:

Time: 15.69870145995352

Let's see if using memoization can help us improve the performance number. In the following code, we use a dict for caching the already computed sums. We also change the parameter passed to the number_sum() function. We want to calculate the sum of the first 300 numbers instead of only the first 30.

Here is the new version of the code, using memoization:

sum_cache = {0:0}

def number_sum(n):

'''Returns the sum of the first n numbers'''

assert(n >= 0), 'n must be >= 0'

if n in sum_cache:

return sum_cache[n]

res = n + number_sum(n-1)

  • Add the value to the cache

sum_cache[n] = res

return res

if __name__ == '__main__':

from timeit import Timer

t = Timer('number_sum(300)', 'from __main__ import number_sum') print('Time: ', t.timeit())

Executing the memoization-based code shows that performance improves dramatically, and is acceptable even for computing large values.

A sample execution, using python number_sum.py, is as follows:

Time: 0.5695815602065222

But there are already a few problems with this approach. While the performance is not an issue any longer, the code is not as clean as it is when not using memoization. And what happens if we decide to extend the code with more math functions and turn it into a module? We can think of several functions that would be useful for our module, for problems such as Pascal's triangle or the Fibonacci numbers suite algorithm.

So, if we wanted a function in the same module as number_sum(), for the Fibonacci numbers suite, using the same memoization technique, we would add code, as follows:

cache_fib = {0:0, 1:1}

def fibonacci(n):

'''Returns the suite of Fibonacci numbers''' assert(n >= 0), 'n must be >= 0'

if n in cache_fib:

return cache_fib[n]

res = fibonacci(n-1) + fibonacci(n-2)

cache_fib[n] = res

return res

Do you notice the problem already? We ended up with a new dict called cache_fib

which acts as our cache for the fibonacci() function, and a function that is more complex than it would be without using memoization. Our module is becoming unnecessarily complex. Is it possible to write these functions keeping them as simple as the naive versions, but achieving a performance similar to the performance of the functions that use memoization?

Fortunately, it is, and the solution is to use the decorator pattern.

First, we create a memoize() decorator as shown in the following example. Our decorator accepts the function fn that needs to be memoized, as an input. It uses a dict named cache as the cached data container. The functools.wraps() function is used for

convenience when creating decorators. It is not mandatory but a good practice to use it, since it makes sure that the documentation, and the signature of the function that is decorated, are preserved (j.mp/funcwraps). The argument list *args is required in this case because the functions that we want to decorate accepts input arguments (such as the n argument for our two functions):

import functools

def memoize(fn): cache = dict()

@functools.wraps(fn)

def memoizer(*args):

if args not in cache:

cache[args] = fn(*args) return cache[args]

return memoizer

Now, we can use our memoize() decorator with the naive version of our functions. This

has the benefit of readable code without performance impact. We apply a decorator using what is known as decoration (or a decoration line). A decoration uses the @name syntax, where name is the name of the decorator that we want to use. It is nothing more than syntactic sugar for simplifying the usage of decorators. We can even bypass this syntax and execute our decorator manually, but that is left as an exercise for you.

So the memoize() decorator can be used with our recursive functions as follows:

@memoize

def number_sum(n):

'''Returns the sum of the first n numbers''' assert(n >= 0), 'n must be >= 0'

if n == 0:

return 0

else:

return n + number_sum(n-1)

@memoize

def fibonacci(n):

'''Returns the suite of Fibonacci numbers''' assert(n >= 0), 'n must be >= 0'

if n in (0, 1):

return n

else:

return fibonacci(n-1) + fibonacci(n-2)

In the last part of the code, via the main() function, we show how to use the decorated functions and measure their performance. The to_execute variable is used to hold a list of tuples containing the reference to each function and the corresponding timeit.Timer()

call (to execute it while measuring the time spent), thus avoiding code repetition. Note how the __name__ and __doc__ method attributes show the proper function names and documentation values, respectively. Try removing the @functools.wraps(fn) decoration

from memoize(), and see if this is still the case.

Here is the last part of the code:

def main():

from timeit import Timer

to_execute = [

(number_sum,

Timer('number_sum(300)', 'from __main__ import number_sum')), (fibonacci,

Timer('fibonacci(100)', 'from __main__ import fibonacci')) ]

for item in to_execute:

fn = item[0]

print(f'Function "{fn.__name__}": {fn.__doc__}') t = item[1]

print(f'Time: {t.timeit()}')

print()

if __name__ == '__main__': main()

Let's recapitulate how we write the complete code of our math module (file mymath.py):

  1. After the import of Python's functools module that we will be using, we define the memoize() decorator function
  2. Then, we define the number_sum() function, decorated using memoize()
  3. We also define the fibonacci() function, as decorated
  4. Finally, we add the main() function, as shown earlier, and the usual trick to call it

Here is a sample output when executing the python mymath.py command:

Nice. We ended up with readable code and acceptable performance. Now, you might argue that this is not the decorator pattern, since we don't apply it at runtime. The truth is that a decorated function cannot be undecorated, but you can still decide at runtime if the decorator will be executed or not. That's an interesting exercise left for you.

Another interesting property of decorators that is not covered in this chapter is that you can decorate a function with more than one decorator. So here's another exercise: create a decorator that helps you to debug recursive functions, and apply it on number_sum() and fibonacci(). In what order are the multiple decorators executed?

Summary

This chapter covered the decorator pattern and its relationship to the Python programming language. We use the decorator pattern as a convenient way of extending the behavior of an object without using inheritance. Python, with its built-in decorator feature, extends the decorator concept even more, by allowing us to extend the behavior of any callable (function, method, or class) without using inheritance or composition.

We have seen a few examples of real-world objects that are decorated, such as cameras. From a software point of view, both Django and Pyramid use decorators to achieve different goals, such as controlling HTTP compression and caching.

The decorator pattern is a great solution for implementing cross-cutting concerns because they are generic and do not fit well into the OOP paradigm. We mentioned several categories of cross-cutting concerns in the Use cases section. In fact, in the Implementation section, a cross-cutting concern was demonstrated: memoization. We saw how decorators can help us to keep our functions clean, without sacrificing performance.

The next chapter covers the bridge pattern.

[ 63 ] )

'Mastering Python Design Patterns' 카테고리의 다른 글

The Facade Pattern  (0) 2023.03.22
The Bridge Pattern  (0) 2023.03.22
The Adapter Pattern  (0) 2023.03.22
Other Creational Patterns  (0) 2023.03.22
The Builder Pattern  (0) 2023.03.22

댓글