본문 바로가기
Practical Python Design Patterns

Proxy Pattern

by 자동매매 2023. 3. 28.

CHAPTER 11 Proxy Pattern

Do you bite your thumb at us, sir?

Abram, Romeo and Juliet

As programs grow, you often find that there are functions that you call very often. When these calculations are heavy or slow, your whole program suffers.

Consider the following example for calculating the Fibonacci sequence for a number n:

def fib(n):

if n < 2:

return 1

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

This fairly simple recursive function has a serious flaw, especially as n becomes really large. Can you figure out what the problem might be?

If you thought that it is the fact that the value of some f(x) would have to be calculated multiple times whenever you want to calculate a Fibonacci number for an n larger than 2, you are spot on.

There are multiple ways we could handle this problem, but I want to use a very specific way, called memoization.

Memoization

Whenever we have a function being called multiple times, with the value repeated, it would be useful to store the response of the calculation so we do not have to go through the process of calculating the value again; we would rather just grab the value we already calculated and return that. This act of saving the result of a function call for later use is called memoization.

133

' Wessel Badenhorst 2017

W. Badenhorst, Practical Python Design Patterns, https://doi.org/10.1007/978-1-4842-2680-3_9
CHAPTER 9 PROXY PATTERN

Now, let s see what effect memoization would have on our simple example case. import time

def fib(n):

if n < 2:

return 1

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

if __name__ == "__main__":

start_time = time.time()

fib_sequence = [fib(x) for x in range(1,80)] end_time = time.time()

print(

"Calculating the list of {} Fibonacci numbers took {} seconds".format(

len(fib_sequence),

end_time - start_time

)

)

Let this run for some time; see the amount of time elapsed just to calculate Fibonacci numbers from 0 to 40.

On my computer, I got the following result:

Calculating the list of 80 Fibonacci numbers took 64.29540348052979 seconds

Some new systems laugh at a mere 80 Fibonacci numbers. If you are in that situation, you could try increasing the numbers in orders of magnitude 80, 800, 8000 until you hit upon a number that shows load without taking forever to complete. Use this number throughout the rest of the chapter.

If we were to cache the results of each call, how would that affect the calculation?

Let s implement the same function as before, but this time we will make use of an extra dictionary to keep track of requests we have calculated before.

134
CHAPTER 9 PROXY PATTERN

import time

def fib_cached1(n, cache):

if n < 2:

return 1

if n in cache:

return cache[n]

cache[n] = fib_cached1(n - 2, cache) + fib_cached1(n - 1, cache) return cache[n]

if __name__ == "__main__":

cache = {}

start_time = time.time()

fib_sequence = [fib_cached1(x, cache) for x in range(0, 80)] end_time = time.time()

print(

"Calculating the list of {} Fibonacci numbers took {} seconds".format(

len(fib_sequence),

end_time - start_time

)

)

Run this code. On my machine, the same series of calculations now gives the following output:

Calculating the list of 80 Fibonacci numbers took 4.7206878662109375e-05 seconds

135
CHAPTER 9 PROXY PATTERN

This is such a good result that we want to create a calculator object that does several mathematical series calculations, including calculating the Fibonacci numbers. See here:

class Calculator(object):

def fib_cached(self, n, cache):

if n < 2:

return 1

try:

result = cache[n] except:

cache[n] = fib_cached(n - 2, cache) + fib_cached(n - 1, cache) result = cache[n]

return result

You know that most users of your object will not know what they should do with the cache variable, or what it means. Having this variable in your method definition for the calculator method might cause confusion. What you want instead is to have a method definition that looks like the first piece of code we looked at, but with the performance benefits of caching.

In an ideal situation, we would have a class that functions as an interface to the calculator class. The client should not be aware of this class, in that the client only

codes toward the interface of the original class, with the proxy providing the same functionality and results as the original class.

The Proxy Pattern

A proxy provides the same interface as the original object, but it controls access to the original object. As part of this control, it can perform other tasks before and after the original object is accessed. This is especially helpful when we want to implement something like memoization without placing any responsibility on the client to understand caching. By shielding the client from the call to the fib method, the proxy allows us to return the calculated result.

136
CHAPTER 9 PROXY PATTERN

Duck typing allows us to create such a proxy by copying the object interface and then using the proxy class instead of the original class.

import time

class RawCalculator(object):

def fib(self, n):

if n < 2:

return 1

return self.fib(n - 2) + self.fib(n - 1)

def memoize(fn):

__cache = {}

def memoized(*args):

key = (fn.__name__, args) if key in __cache:

return __cache[key]

__cache[key] = fn(*args) return __cache[key]

return memoized

class CalculatorProxy(object):

def __init__(self, target):

self.target = target

fib = getattr(self.target, ’fib’) setattr(self.target, ’fib’, memoize(fib))

def __getattr__(self, name):

return getattr(self.target, name)

137
CHAPTER 9 PROXY PATTERN

if __name__ == "__main__":

calculator = CalculatorProxy(RawCalculator())

start_time = time.time()

fib_sequence = [calculator.fib(x) for x in range(0, 80)] end_time = time.time()

print(

"Calculating the list of {} Fibonacci numbers took {} seconds".format(

len(fib_sequence),

end_time - start_time

)

)

I must admit, this is not a trivial piece of code, but let s step through it and see what is happening.

First, we have the RawCalculator class, which is our imagined calculation object.

At the moment, it only contains the Fibonacci calculation, but you could imagine it containing many other recursively defined series and sequences. As before, this method uses a recursive call.

Next, we define a closure that wraps the function call, but let s leave that for later. Finally, we have the CalculatorProxy class, which upon initialization takes the

target object as a parameter, sets an attribute on the proxy object, and then proceeds to override the fib method of the target object with a memoized version of the fib method. Whenever the target object calls its fib() method, the memoized version gets called.

Now, the memoize function takes a function as a parameter. Next, it initializes an empty dictionary. We then define a function that takes a list of arguments, gets the

name of the function passed to it, and creates a tuple containing the function name and received arguments. This tuple then forms the key to the __cache dictionary. The value represents the value returned by the function call.

The memoized function first checks if the key is already in the cache dictionary. If

it is, there is no need to recalculate the value, and the value is returned. If the key is not found, the original function is called, and the value is added to the dictionary before it is returned.

138
CHAPTER 9 PROXY PATTERN

The memoize function takes a regular old function as a parameter, then it records the result of calling the received function. If needed it calls the function and receives the newly calculated values before returning a new function with the result saved if that value should be needed in future.

The final result is seen here:

Calculating the list of 80 Fibonacci numbers took 8.20159912109375e-05 seconds

I really like the fact that the memoize function can be used with any function passed to it. Having a generic piece of code like this is helpful, as it allows you to memoize many different functions without altering the code. This is one of the main aims of object- oriented programming. As you gain experience, you should also build up a library

of interesting and useful pieces of code you can reuse. Ideally, you want to package

these into your own libraries so you can do more in less time. It also helps you to keep improving.

Now that you understand the proxy pattern, let s look at the different types of proxies available to us. We already looked at a cache proxy in detail, so what remains are the following:

Remote proxy

Virtual proxy

Protection proxy

Remote Proxy

When we want to abstract the location of an object, we can use a remote proxy. The remote object appears to be a local resource to the client. When you call a method on the proxy, it transfers the call to the remote object. Once the result is returned, the proxy makes this result available to the local object.

Virtual Proxy

Sometimes objects may be expensive to create, and they may not be needed until later in the program s execution. When it is helpful to delay object creation, a proxy can be used. The target object can then be created only once it is needed.

139
CHAPTER 9 PROXY PATTERN

Protection Proxy

Many programs have different user roles with different levels of access. By placing a proxy between the target object and the client object, you can restrict access to information and methods on the target object.

Parting Shots

You saw how a proxy could take many forms, from the cache proxy we dealt with in

this chapter to the network connection. Whenever we want to control how an object or resource is accessed in a way that is transparent from the client s perspective, the proxy pattern should be considered. In this chapter you saw how the proxy pattern wraps

other objects and alters the way they execute in a way that is invisible to the user of these functions, since they still take the same parameters as input and returns the same results as output.

The proxy pattern typically has three parts:

The client that requires access to some object

e object the client wants to access

The proxy that controls access to the object

The client instantiates the proxy and makes calls to the proxy as if it were the object

itself.

Because Python is dynamically typed, we are not concerned with defining some common interface or abstract class, since we are only required to provide the same attributes as the target object in order for the proxy to be treated exactly like the target object by the client code.

It is helpful to think about proxies in terms we are already familiar with, like a web proxy. Once connected to the proxy, you, the client, are not aware of the proxy; you can access the Internet and other network resources as if there were no proxy. This is the key differentiator between the proxy pattern and the adapter pattern. With the proxy pattern, you want the interface to remain constant, with some actions taking place in the background. Conversely, the adapter pattern is targeted at changing the interface.

140
CHAPTER 9 PROXY PATTERN

Exercises

Add more functions to the RawCalculator class so it can create other

sequences.

Expand the CalculatorProxy class to handle the new

series-generation functions you added. Do they also use the memoize function out of the box?

Alter the __init__() function to iterate over the attributes of the

target object and automatically memoize the callable attributes.

Count how many times each of the RawCalculator methods is called

during a program; use this to calculate how many calls are made to the fib() method.

Check the Python standard library for other ways to memoize

functions.

141

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

Command Pattern  (0) 2023.03.28
Chain of Responsibility Pattern  (0) 2023.03.28
Facade Pattern  (0) 2023.03.28
Decorator Pattern  (0) 2023.03.28
Adapter Pattern  (0) 2023.03.28

댓글