1

I have a working decorator for running a method in a while True and it works fine on a regular function. The problem occurs when i try to decorate a function of an instance.

This is the decorator:

from threading import Thread


def run_in_while_true(f):
    def decorator(break_condition=False):
        def wrapper(*args, **kwargs):
            while True:
                if break_condition:
                    return
                f(*args, **kwargs)
         return wrapper
    return decorator


class A(object):
    @run_in_while_true
    def print_ch(self, ch):
         print ch

@run_in_while_true
def print_with_dec(ch):
     print ch


print_with_dec()('f')  # Call 1
# If i would want to pass a break condition i would write this
print_with_dec(1==1 and 2*2==4)('f')

a = A()
a.print_ch()('4')  # Call 2

`

Call 1 runs as expected and prints f a lot. Call 2 for some reason gets the self parameter where break_condition is and becuase of that the check for break_condition is true and the function returns.

In what way do i need to change the decorator so it works with objects as well? Thanks in advance

Koby 27
  • 1,049
  • 1
  • 8
  • 17
  • How would you set the break condition to `True` here? How do you feed in `ch` ? This code seems to be missing quite a few things to become a [mcve]. Maybe this is a [xy-problem](https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem) ? What problem do you want to solve with this code? – Patrick Artner Feb 08 '19 at 07:13
  • Maybe tag additionally with `python 2.[x, 6, 7]` - you seem to be using one of those. – Patrick Artner Feb 08 '19 at 07:17
  • I am writing an online game library and i wish to run multiple threads at once which will all quit based on some condition. The decorators goal is to make the code cleaner because the functions won't have the while True in them and i won't have to create a new thread each time. – Koby 27 Feb 08 '19 at 07:18
  • @PatrickArtner In call 1 for example, i could set break_condition to True like this: `print_with_dec(True)('f')`, as excpected the console is empty. – Koby 27 Feb 08 '19 at 07:21
  • 2
    That `break_condition` makes no sense. Are you under the impression that you would be able to set one thread's `break_condition` to `False` by calling `print_with_dec(False)('f')` in another thread? It doesn't work like that. Currently, your function either halts immediately or runs forever. – user2357112 Feb 08 '19 at 07:26
  • 1
    Or are you under the impression that you can pass an expression as `break_condition` and have it reevaluated on every iteration? That's not how Python parameter passing works. If you pass `x==4` as the `break_condition`, that expression will be evaluated once, and the resulting value of either `True` or `False` is passed as the argument value. It won't be evaluated repeatedly. – user2357112 Feb 08 '19 at 07:29
  • @user2357112 I haven't noticed it until now, you're correct. How would I change it? As you can see im quite new to decorators. – Koby 27 Feb 08 '19 at 07:30
  • @Koby - your thread can have attached properties to check: see f.e. https://stackoverflow.com/a/36499538/7505395 to the question [how-to-stop-a-looping-thread-in-python](https://stackoverflow.com/questions/18018033/how-to-stop-a-looping-thread-in-python) - it lacks your "decorator" approach so it is no 100% dupe – Patrick Artner Feb 08 '19 at 07:31
  • @user2357112 I fixed what you pointed out by using eval() but the self parameter is still being passed as break_condition and i don't understand why. – Koby 27 Feb 08 '19 at 07:40

2 Answers2

0

Your resulting code is really strange-looking:

a.print_ch()('4')  # Call 2

This is because you have one extra layer in your decorator:

def run_in_while_true(f):
    def decorator(break_condition=False):
        def wrapper(*args, **kwargs):

The @run_in_while_true decorator is going to return decorator, which has to be called to return wrapper, which has to be called to evaluate a result. The @run_in_while_true gets called automatically as part of the decoration. The other two require two sets of parens, as shown in your code.

This is a problem because method invocation, like a.print_ch(), automatically passes the invocant to the first call:

a.print_ch()('4')
# is much the same as
# A.print_ch(self=a)('4')

which explains why you are getting your invocant in your break_condition.

I'd suggest that you try to unify the two inner functions. Just pass a named parameter like break_condition or break_when or break_if to the function/method, and have the wrapper intercept that one value: import functools

def run_until_done(func):
    @functools.wraps
    def wrapper(*args, break_if=None, **kwargs):
        done = break_if if callable(break_if) else lambda: break_if

        while not done():
            func(*args, **kwargs)
     return wrapper

@run_until_done
def print_with_dec(ch):
    print ch

print_with_dec('4', break_if=lambda: 1==1 and is_done())
aghast
  • 14,785
  • 3
  • 24
  • 56
  • First of all, thanks for taking the time to reply. Is it possible that your code is not compatible with python 2.7?I got an error for the way the parameters of wrapper are ordered because I can't put a regular parameter after a starred parameter but then i got this error: `TypeError: update_wrapper() got an unexpected keyword argument 'break_if'` – Koby 27 Feb 08 '19 at 08:31
0

Thanks for everyone helping, after researching more about the way a function is called from an object I wrote these final decorators. Their both work for regular functions and methods of objects. One runs the function in a loop until the condition is met, the other runs the first one in a thread so the program does not wait.

The decorators

def loop_in_while_oop(f):
    """ Runs a function in a loop, params have to be passed by name"""
    def decorated(self=None, break_if=None, *args,**kwargs):
        """
        :param self: Will be passed automatically if needed
        :param break_if: Lambada expression for when to stop running the while loop
        """
        done = break_if if callable(break_if) else lambda: break_if
        while not done():
            if self is not None:
                f(self, *args, **kwargs)
            else:
                f(*args, **kwargs)
    return decorated

def loop_in_thread_oop(f):
    """ Runs function in a loop in a thread, MUST: pass arguments by name"""
    def decorated(self=None, break_if=lambda: False, *args, **kwargs):
        """
        :param self: Will be passed automatically if needed
        :param break_if: Lambada expression for when to stop running the while loop, if value not passed will run forever
        """
        f1 = loop_in_while_oop(f)
        t = Thread(target=f1, args=args, kwargs=dict(self=self, break_if=break_if, **kwargs))
        t.start()
    return decorated

Using the decorators

class SomeObj(object):

    @loop_in_thread_oop
    def print_c(self, c):
        print c


@loop_in_thread_oop
def p1(f):
    print f


@loop_in_thread_oop
def p2(f):
    print f

if __name__ == '__main__':
    a = SomeObj()
    start = time.time()
    a.print_c(c='a')  # Will run forever because break_if was not specified
    p1(f='3', break_if=lambda: time.time() - start > 3)  # Will stop after 3 seconds
    p2(f='5', break_if=lambda: time.time() - start > 5)  # Will stop after 5 seconds

Output:

  • Between 0-3 seconds: Prints a35 (Order is not constant)
  • Between 3-5 seconds: Prints a5 (Order is not constant)
  • Between 3-5 seconds: Prints a
Koby 27
  • 1,049
  • 1
  • 8
  • 17