0

I have a run_once decorator from : Efficient way of having a function only execute once in a loop A parent class like:

class Parent:

    @run_once
    def test(self,x):
        print(x)

    def call_test(self,):
        for x in [1,3,4]:
            self.test(x)

Class Child1(Parent):

    def child_method(self,): 
        self.call_test()

Class Child2(Parent):

    def child_method(self,): 
        self.call_test()

Now When I use pytest to test the both child classes

def test_child1():
    c1 = Child1()
    c1.child_method()
    # assert that x is printed once
    # tried to reset `test.has_run` = False # didn't work
    # throws: *** AttributeError: 'method' object has no attribute 'has_run'

def test_child2():
    c2 = Child2()
    c2.child_method()
    # assert that x is printed once # FAILS because the test.has_run is already set in `c1.child_method()` call.

Is there a way to make run_once on object level ?

triandicAnt
  • 1,328
  • 2
  • 15
  • 40
  • I'm confused, can you explain what this would be used for? – AMC Jan 09 '20 at 00:54
  • @AMC I had some use-case like: Fetch all data from a database table. I added pagesize(10K as can't load all data in memory). so query will called multiple times. When the first query returns a result, I want to make a model based on the result field_name-value dict.(and this would be one time- so with `@run_once` annotation). So instead of calling the first query outside of `while data_is_available` loop, I am calling each query inside loop with run_once method as well, and the run_once will execute just once-creating the model from first query result. – triandicAnt Jan 09 '20 at 21:01

2 Answers2

0

I was able to set the has_run attribute using the following:

Parent.test.has_run = False

Earlier I was trying:

c1 = Child1()
c1.child_method()
C1.test.has_run = False
throws: *** AttributeError: 'method' object has no attribute 'has_run'

but it seems the method belongs to class, even if test is an instance method.

triandicAnt
  • 1,328
  • 2
  • 15
  • 40
0

I had a similar problem, and this question helped me to come up with a class decorator solution. I needed to set class attributes within the decorated functions. I am using Python 3.9.

import functools

class run_once(object):

    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func

    def __call__(self, instance, *args, **kwargs):
        print (f"In run_once call")
        fname = self.func.__name__
        rname = f"{fname}_has_run"
        if not hasattr(instance, rname):
            self.func(instance, *args, **kwargs)
            setattr(instance, rname, True)
        return


    def __get__(self, instance, instancetype):
        return functools.partial(self.__call__, instance)

This works for decorating functions within another class. The check for hasattr(instance, rname) ensures that the decorated function is called everytime the class is instantiated. Here is a MWE:

class BAR(object):

    def __init__(self, a, b, c): 
        self.a = a 
        self.b = b 
        self.c = c 
        self.calculate_sum(self.c)
        self.calculate_product()

    def __hash__(self):
        return hash((self.a, self.b, self.c))

    @property
    def sum(self):
        self.calculate_sum(self.c)
        return self._sum

    @property
    def product(self):
        self.calculate_product()
        return self._product

    @run_once
    def calculate_sum(self, c): 
        print ("In BAR.calculate_sum()")
        self._sum = self.a + self.b + c 
        print (f"In calculate_sum, Sum is {self._sum}")
        return


    @run_once
    def calculate_product(self):
        print ("In BAR.calculate_product()")
        self._product = self.a * self.b * self.c
        print (f"In calculate_product, Product is {self._product}")
        return


if __name__ == '__main__':
    mbar1 = BAR(1, 2, 3)
    msum  = mbar1.sum
    mprod = mbar1.product
    print (f"In main first call, sum is {msum}")
    print (f"In main first call, product is {mprod}")
    mbar2 = BAR(4, 5, 6)
    msum  = mbar2.sum
    mprod = mbar2.product
    print (f"In main second call, sum is {msum}")
    print (f"In main second call, product is {mprod}")
banskt
  • 416
  • 3
  • 7
  • 17