0

The goal of my code is to try and get my function to only run on the first call, and after to just return the answer of the first call. However it appears "answer" is not staying as the appended version for the second call. How should I fix this?

class Attempt:
    def __init__(self, stop):
        self.stop = stop

answer = Attempt([])

def oncefunc(func):
    if answer.stop == []:
        answer.stop.append(func)
        return answer.stop[0]
    else:
        return answer.stop[0]

Here's an example of what I would be running:

def mult(a1, a2):
    return a1 * a2
multonce = oncefunc(mult)

For example, I would want to call multOnce(1,2) and then multOnce(3,4), but for both calls I want to return 2.

CaitlinM
  • 21
  • 1
  • 6
  • You're not doing anything with calling time nor the returned value. – Mazdak May 24 '17 at 04:59
  • 2
    What you try to implement is called Memoization. Related question: [What is memoization and how can I use it in Python?](https://stackoverflow.com/questions/1988804/what-is-memoization-and-how-can-i-use-it-in-python) – Tomalak May 24 '17 at 05:03
  • You're comparing to an empty list. I think what you want is `isinstance` Use `if isinstance(answer.stop, list):` – GH05T May 24 '17 at 05:28

4 Answers4

1

The thing that you want to do is called "memoization". First of all, in your code the function never actually gets called, you are just storing the function object itself. In order to fix that you need to create a wrapper function inside oncefunc() and return it:

(here I am not considering the keyword arguments to func for simplicity)

class Attempt:
    def __init__(self, stop):
        self.stop = stop

answer = Attempt([])

def oncefunc(func):
    def wrapper(*args):
        if answer.stop == []:
            answer.stop.append(func(*args))
            return answer.stop[0]
        else:
            return answer.stop[0]
    return wrapper

def mult(a1, a2):
    print("calculating")
    return a1 * a2

multonce = oncefunc(mult)

print(multonce(1, 2))
print(multonce(1, 2))

Then we have the next problem: the answer gets stored in the same place for any arguments of func! So if you call multonce with different arguments the second time, it will return the same value. This can be fixed by keeping a dictionary with keys being the argument tuples:

class Attempt:
    def __init__(self):
        self.answers = {}

answer = Attempt()

def oncefunc(func):
    def wrapper(*args):
        if args not in answer.answers:
            answer.answers[args] = func(*args)
            return answer.answers[args]
        else:
            return answer.answers[args]
    return wrapper

def mult(a1, a2):
    print("calculating")
    return a1 * a2

multonce = oncefunc(mult)

print(multonce(1, 2))
print(multonce(1, 2))
print(multonce(3, 4))

And the last thing is that keeping the answer database outside makes it global for all functions wrapped in oncefunc. It is much more convenient to keep it in a closure, so that it is unique for every application of oncefunc:

def oncefunc(func):
    answers = {}
    def wrapper(*args):
        if args not in answers:
            answers[args] = func(*args)
            return answers[args]
        else:
            return answers[args]
    return wrapper

def mult(a1, a2):
    print("calculating")
    return a1 * a2

multonce = oncefunc(mult)

print(multonce(1, 2))
print(multonce(1, 2))
print(multonce(3, 4))

Of course, this is a very common pattern, so Python already has an implementation of it: lru_cache:

from functools import lru_cache

@lru_cache(maxsize=None)
def mult(a1, a2):
    print("calculating")
    return a1 * a2

print(mult(1, 2))
print(mult(1, 2))
print(mult(3, 4))
fjarri
  • 9,546
  • 39
  • 49
  • Why do you even need a class. Can this not be done with a closure? Your method requires defining and instantiating a class, which is not used for anything other than storing a dictionary/list – Abid Hasan May 24 '17 at 05:13
  • If you read past the first code block, you will see that I switched to a closure later. I started from the class because that's what the original question had. – fjarri May 24 '17 at 05:15
  • Thank you so much! I actually wanted it to return only the first value every time so your first example worked perfectly. Thank you for the help! – CaitlinM May 24 '17 at 05:22
  • Glad it helped, although I'm not sure what a practical application of this strategy would be. Still, consider using a closure like in the third code block (but just keeping a single answer there, not a dictionary indexed by `args`), it makes the code cleaner. – fjarri May 24 '17 at 05:27
0

I think, class attribute will be more reliable in your case as you need to maintain same list over all calls down the line.

Since stop = [] is defined at class level, every call will use the same reference of the list stop as below.

class Attempt:
    stop = []
    # def __init__(self, stop):
    #     self.stop = stop

answer = Attempt()

def oncefunc(func):

    if answer.stop == []:
        print('entered first')
        answer.stop.append(func)
        return answer.stop[0]
    else:
        print('entered second')
        answer.stop.append(func)
        return answer.stop



def mult(a1, a2):
    return a1 * a2

multonce = oncefunc(mult)
multonce = oncefunc(mult)

print(multonce)

OUTPUT:

entered first
entered second
[<function mult at 0x7fc4e3a91cf8>, <function mult at 0x7fc4e3a91cf8>]

Is this answer your concerns?

Sijan Bhandari
  • 2,941
  • 3
  • 23
  • 36
  • Ok I see what you are doing, but what if I want to call the function for two different sets of values and I want it to return the value of the first call both times. For example: multOnce(1,2) and then multOnce(3,4), but for both calls I want to return 2 – CaitlinM May 24 '17 at 05:16
0

You pass method as parameter into your method oncefunc, you didn't provide any value for method mult, your answer.stop[0] is the method mult object, you can provide 2 parameters for it, for instance:

class Attempt:
    def __init__(self, stop):
        self.stop = stop

answer = Attempt([])

def oncefunc(func):
    if answer.stop == []:
        answer.stop.append(func)
        return answer.stop[0](3,4)
    else:
        return answer.stop[0](3,4)

def mult(a1, a2):
    return a1 * a2

print(oncefunc(mult))  # this print out 12

Alternatively, keep oncefunc unmodified, and when you invoke mult with 2 numerical values as:

oncefunc(mult(3,4))
Haifeng Zhang
  • 30,077
  • 19
  • 81
  • 125
  • Ok I see what you are doing, but what if I want to call the function for two different sets of values and I want it to return the value of the first call both times. For example: multOnce(1,2) and then multOnce(3,4), but for both calls I want to return 2 – CaitlinM May 24 '17 at 05:17
0

Why don't you use a closure to do this rather than a class?

#This is the outer function, that keeps track of whether its been run or not
def outer(f):
    #You can use a list or boolean
    has_been_run = []
    #wrapper function
    def wrapper(*args, **kwargs):
        if not(has_been_run):
            print "I haven't been run before!"
            #Store the results of the function call
            has_been_run.append(f(*args, **kwargs))
        else:
            #This has been run before, so just return the list
            print "I have been run before!"
            return has_been_run
        #Return the original function
        return f(*args, **kwargs)    
    return wrapper


@outer
def m(a, b):
    return a + b
Abid Hasan
  • 648
  • 4
  • 10