开发者

Efficient way of having a function only execute once in a loop

开发者 https://www.devze.com 2023-01-23 20:35 出处:网络
At the moment, I\'m doing stuff like the following, which is getting tedious: run_once = 0 while 1: if run_once == 0:

At the moment, I'm doing stuff like the following, which is getting tedious:

run_once = 0
while 1:
    if run_once == 0:
        myFunction()
        run_once = 1:

I'm guessing there is some more accepted way of handling this stuff?

What I'm looking for is having a fu开发者_开发知识库nction execute once, on demand. For example, at the press of a certain button. It is an interactive app which has a lot of user controlled switches. Having a junk variable for every switch, just for keeping track of whether it has been run or not, seemed kind of inefficient.


I would use a decorator on the function to handle keeping track of how many times it runs.

def run_once(f):
    def wrapper(*args, **kwargs):
        if not wrapper.has_run:
            wrapper.has_run = True
            return f(*args, **kwargs)
    wrapper.has_run = False
    return wrapper


@run_once
def my_function(foo, bar):
    return foo+bar

Now my_function will only run once. Other calls to it will return None. Just add an else clause to the if if you want it to return something else. From your example, it doesn't need to return anything ever.

If you don't control the creation of the function, or the function needs to be used normally in other contexts, you can just apply the decorator manually as well.

action = run_once(my_function)
while 1:
    if predicate:
        action()

This will leave my_function available for other uses.

Finally, if you need to only run it once twice, then you can just do

action = run_once(my_function)
action() # run once the first time

action.has_run = False
action() # run once the second time


Another option is to set the func_code code object for your function to be a code object for a function that does nothing. This should be done at the end of your function body.

For example:

def run_once():  
   # Code for something you only want to execute once
   run_once.func_code = (lambda:None).func_code

Here run_once.func_code = (lambda:None).func_code replaces your function's executable code with the code for lambda:None, so all subsequent calls to run_once() will do nothing.

This technique is less flexible than the decorator approach suggested in the accepted answer, but may be more concise if you only have one function you want to run once.


Run the function before the loop. Example:

myFunction()
while True:
    # all the other code being executed in your loop

This is the obvious solution. If there's more than meets the eye, the solution may be a bit more complicated.


I'm assuming this is an action that you want to be performed at most one time, if some conditions are met. Since you won't always perform the action, you can't do it unconditionally outside the loop. Something like lazily retrieving some data (and caching it) if you get a request, but not retrieving it otherwise.

def do_something():
    [x() for x in expensive_operations]
    global action
    action = lambda : None

action = do_something
while True:
    # some sort of complex logic...
    if foo:
        action()


There are many ways to do what you want; however, do note that it is quite possible that —as described in the question— you don't have to call the function inside the loop.

If you insist in having the function call inside the loop, you can also do:

needs_to_run= expensive_function
while 1:
    …
    if needs_to_run: needs_to_run(); needs_to_run= None
    …


I've thought of another—slightly unusual, but very effective—way to do this that doesn't require decorator functions or classes. Instead it just uses a mutable keyword argument, which ought to work in most versions of Python. Most of the time these are something to be avoided since normally you wouldn't want a default argument value to change from call-to-call—but that ability can be leveraged in this case and used as a cheap storage mechanism. Here's how that would work:

def my_function1(_has_run=[]):
    if _has_run: return
    print("my_function1 doing stuff")
    _has_run.append(1)

def my_function2(_has_run=[]):
    if _has_run: return
    print("my_function2 doing some other stuff")
    _has_run.append(1)

for i in range(10):
    my_function1()
    my_function2()

print('----')
my_function1(_has_run=[])  # Force it to run.

Output:

my_function1 doing stuff
my_function2 doing some other stuff
----
my_function1 doing stuff

This could be simplified a little further by doing what @gnibbler suggested in his answer and using an iterator (which were introduced in Python 2.2):

from itertools import count

def my_function3(_count=count()):
    if next(_count): return
    print("my_function3 doing something")

for i in range(10):
    my_function3()

print('----')
my_function3(_count=count())  # Force it to run.

Output:

my_function3 doing something
----
my_function3 doing something


Here's an answer that doesn't involve reassignment of functions, yet still prevents the need for that ugly "is first" check.

__missing__ is supported by Python 2.5 and above.

def do_once_varname1():
    print 'performing varname1'
    return 'only done once for varname1'
def do_once_varname2():
    print 'performing varname2'
    return 'only done once for varname2'

class cdict(dict):
    def __missing__(self,key):
        val=self['do_once_'+key]()
        self[key]=val
        return val

cache_dict=cdict(do_once_varname1=do_once_varname1,do_once_varname2=do_once_varname2)

if __name__=='__main__':
    print cache_dict['varname1'] # causes 2 prints
    print cache_dict['varname2'] # causes 2 prints
    print cache_dict['varname1'] # just 1 print
    print cache_dict['varname2'] # just 1 print

Output:

performing varname1
only done once for varname1
performing varname2
only done once for varname2
only done once for varname1
only done once for varname2


One object-oriented approach and make your function a class, aka as a "functor", whose instances automatically keep track of whether they've been run or not when each instance is created.

Since your updated question indicates you may need many of them, I've updated my answer to deal with that by using a class factory pattern. This is a bit unusual, and it may have been down-voted for that reason (although we'll never know for sure because they never left a comment). It could also be done with a metaclass, but it's not much simpler.

def RunOnceFactory():
    class RunOnceBase(object): # abstract base class
        _shared_state = {} # shared state of all instances (borg pattern)
        has_run = False
        def __init__(self, *args, **kwargs):
            self.__dict__ = self._shared_state
            if not self.has_run:
                self.stuff_done_once(*args, **kwargs)
                self.has_run = True
    return RunOnceBase

if __name__ == '__main__':
    class MyFunction1(RunOnceFactory()):
        def stuff_done_once(self, *args, **kwargs):
            print("MyFunction1.stuff_done_once() called")

    class MyFunction2(RunOnceFactory()):
        def stuff_done_once(self, *args, **kwargs):
            print("MyFunction2.stuff_done_once() called")

    for _ in range(10):
        MyFunction1()  # will only call its stuff_done_once() method once
        MyFunction2()  # ditto

Output:

MyFunction1.stuff_done_once() called
MyFunction2.stuff_done_once() called

Note: You could make a function/class able to do stuff again by adding a reset() method to its subclass that reset the shared has_run attribute. It's also possible to pass regular and keyword arguments to the stuff_done_once() method when the functor is created and the method is called, if desired.

And, yes, it would be applicable given the information you added to your question.


Assuming there is some reason why myFunction() can't be called before the loop

from itertools import count
for i in count():
    if i==0:
        myFunction()


Here's an explicit way to code this up, where the state of which functions have been called is kept locally (so global state is avoided). I don't much like the non-explicit forms suggested in other answers: it's too surprising to see f() and for this not to mean that f() gets called.

This works by using dict.pop which looks up a key in a dict, removes the key from the dict, and takes a default value to use in case the key isn't found.

def do_nothing(*args, *kwargs):
    pass

# A list of all the functions you want to run just once.
actions = [
    my_function,
    other_function
]
actions = dict((action, action) for action in actions)

while True:
    if some_condition:
        actions.pop(my_function, do_nothing)()
    if some_other_condition:
        actions.pop(other_function, do_nothing)()


I use cached_property decorator from functools to run just once and save the value. Example from the official documentation https://docs.python.org/3/library/functools.html

class DataSet:

    def __init__(self, sequence_of_numbers):
        self._data = tuple(sequence_of_numbers)

    @cached_property
    def stdev(self):
        return statistics.stdev(self._data)


You can also use one of the standard library functools.lru_cache or functools.cache decorators in front of the function:

from functools import lru_cache

@lru_cache
def expensive_function():
   return None

https://docs.python.org/3/library/functools.html


If I understand the updated question correctly, something like this should work

def function1():
    print "function1 called"

def function2():
    print "function2 called"

def function3():
    print "function3 called"

called_functions = set()
while True:
    n = raw_input("choose a function: 1,2 or 3 ")
    func = {"1": function1,
            "2": function2,
            "3": function3}.get(n)

    if func in called_functions:
        print "That function has already been called"
    else:
        called_functions.add(func)
        func()


You have all those 'junk variables' outside of your mainline while True loop. To make the code easier to read those variables can be brought inside the loop, right next to where they are used. You can also set up a variable naming convention for these program control switches. So for example:

#                                  # _already_done checkpoint logic
    try:
        ran_this_user_request_already_done
    except:
        this_user_request()
        ran_this_user_request_already_done = 1

Note that on the first execution of this code the variable ran_this_user_request_already_done is not defined until after this_user_request() is called.


A simple function you can reuse in many places in your code (based on the other answers here):

def firstrun(keyword, _keys=[]):
    """Returns True only the first time it's called with each keyword."""
    if keyword in _keys:
        return False
    else:
        _keys.append(keyword)
        return True

or equivalently (if you like to rely on other libraries):

from collections import defaultdict
from itertools import count
def firstrun(keyword, _keys=defaultdict(count)):
    """Returns True only the first time it's called with each keyword."""
    return not _keys[keyword].next()

Sample usage:

for i in range(20):
    if firstrun('house'):
        build_house() # runs only once
if firstrun(42): # True
    print 'This will print.'
if firstrun(42): # False
    print 'This will never print.'


I've taken a more flexible approach inspired by functools.partial function:

DO_ONCE_MEMORY = []

def do_once(id, func, *args, **kwargs):
    if id not in DO_ONCE_MEMORY:
        DO_ONCE_MEMORY.append(id)
        return func(*args, **kwargs)
    else:
        return None

With this approach you are able to have more complex and explicit interactions:

do_once('foobar', print, "first try")
do_once('foo', print, "first try")
do_once('bar', print, "second try")
# first try 
# second try 

The exciting part about this approach it can be used anywhere and does not require factories - it's just a small memory tracker.


Depending on the situation, an alternative to the decorator could be the following:

from itertools import chain, repeat

func_iter = chain((myFunction,), repeat(lambda *args, **kwds: None))
while True:
    next(func_iter)()

The idea is based on iterators, which yield the function once (or using repeat(muFunction, n) n-times), and then endlessly the lambda doing nothing.

The main advantage is that you don't need a decorator which sometimes complicates things, here everything happens in a single (to my mind) readable line. The disadvantage is that you have an ugly next in your code.

Performance wise there seems to be not much of a difference, on my machine both approaches have an overhead of around 130 ns.


If the condition check needs to happen only once you are in the loop, having a flag signaling that you have already run the function helps. In this case you used a counter, a boolean variable would work just as fine.

signal = False
count = 0 
def callme(): 
     print "I am being called"  
while count < 2: 
     if signal == False : 
         callme()
         signal = True
     count +=1


I'm not sure that I understood your problem, but I think you can divide loop. On the part of the function and the part without it and save the two loops.

0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号