0

I have the following situation : I want to time a method for multiple objects :

class Foo():
  def method(self):
    pass
class Bar():
  def method(self, x):
    print(x)

More specifically, I want to have a list that gets appended with the execution time every time the method is called.

So basically, I want to have a function time_method such that the following code will print the time of 3 executions of the method method for the x object.

x = Foo()
y = time_method(x, 'method')
for i in range(3):
  y.method()
print(y.get_time())

So basically y would need to act exactly like x everywhere except when y.method() is called in which case it would also record the time of execution. The list of time of execution could then be accessed by the get_time method specific to y. This time_method function should work on any object.

Statistic Dean
  • 4,861
  • 7
  • 22
  • 46
  • Possible duplicate of [Measure time elapsed in Python?](https://stackoverflow.com/questions/7370801/measure-time-elapsed-in-python) – Some programmer dude Aug 08 '19 at 09:26
  • I'm not sure if I understood what you want to do exactly, but you should check out the [timeit](https://docs.python.org/3/library/timeit.html) module. – mkrieger1 Aug 08 '19 at 09:26
  • Possible duplicate of [Python time measure function](https://stackoverflow.com/questions/5478351/python-time-measure-function) – mkrieger1 Aug 08 '19 at 09:28
  • @mkrieger1 Yeah but he wants to have a class which inherits a method, but records a time of execution as well. I don't think it's a duplicate, because the issue is with the structure, not measuring the time itself. – Ardweaden Aug 08 '19 at 09:30
  • Also related: https://stackoverflow.com/questions/1622943/timeit-versus-timing-decorator – mkrieger1 Aug 08 '19 at 09:30
  • 1
    Then the specific problem should be made more clear in the question. – mkrieger1 Aug 08 '19 at 09:31
  • @mkrieger1 Personally I find it very clear to be honest. – Ardweaden Aug 08 '19 at 09:34
  • Yes the issue is not measuring time but about taking any object with a method and call `time_method` on it to be able to record time taken by the call to this method. – Statistic Dean Aug 08 '19 at 09:35

2 Answers2

1

I think this should work:

import inspect

class time_method:
    def __init__(self,timedClass,method):
        self.timedClass = timedClass
        self.methodName = method
        self.measuredTimes = []
        self.all_methods = inspect.getmembers(timedClass, predicate=inspect.ismethod)
        self.set_all_methods();
        setattr(self, method, self.timeMethod)

    def timeMethod(self,*args,**kwargs):
        start = time.time()
        res=getattr(self.timedClass, self.methodName)(*args,**kwargs)
        end = time.time()
        self.measuredTimes.append(end-start)
        return res

    def get_time(self):
        return self.measuredTimes

    def set_all_methods(self):
        for method in self.all_methods:
            setattr(self, *method)

    def __getitem__(self,*args,**kwargs):
        return self.timeMethod(*args,**kwargs)

    def __repr__(self,*args,**kwargs):
        return self.timeMethod(*args,**kwargs)

All special functions that are supposed to work must be implemented in the pattern shown above. Of course, when initialising time_method one has to say:

y = time_method(x, '__getitem__')

Then you can call as:

y[something]
Ardweaden
  • 857
  • 9
  • 23
1

After some trial and errors, this seems to be working :

import types
import time

def time_method(x, method):
    setattr(x, f'time_of_{method}', [])
    if method not in get_object_methods(x):
        print(f'Object {x} doesn\'t have method {method}')
    else:
        old_method = getattr(x, method)
        def new_method(self, *args, **kwargs):
            start = time.time()
            res = old_method(*args, **kwargs) 
            end = time.time()
            getattr(x, f'time_of_{method}').append(end-start)
            return res
        setattr(x, method, types.MethodType(new_method, x))
    return x

The only 'issue' is that it modifies x in place but it shouldn't be a problem in my use case.

Note : This doesn't work for method that are bound to the class like __getitem__ Is it possible to override __getitem__ at instance level in Python?

Note 2 : With multiprocessing, things get a little more tricky, indeed, if your object is sent to multiple thread the f'time_of_{method}' attribute is not shared and essentially never update. A solution is to use Manager

import multiprocessing
manager = multiprocessing.Manager()

And to replace [] by manager.list() at initialization.

Statistic Dean
  • 4,861
  • 7
  • 22
  • 46
  • Ah, I thought you were supposed to use classes, which I do think it's a bit more elegant and actually works *exactly* like specified in the OP. Interesting solution however. – Ardweaden Aug 08 '19 at 10:15
  • So you'd like to call `y[4]` for example? Because `y.__getitem__(4)` does work. – Ardweaden Aug 09 '19 at 08:03
  • Absolutely, the idea is to pass x into foreign code to time some things, in that foreign code, `__getitem__` is never used directly but is called at class level by `[]` – Statistic Dean Aug 09 '19 at 08:30
  • Sadly, I don't think there is a way around it, other than implementing all the special methods, that you expect to be used. I edited my code, so now you can call `y[4]` and added another example for `__repr__` just to show that all special methods would be implemented identically. – Ardweaden Aug 09 '19 at 09:07
  • Well, in your case y doesn't act like x, for instance, if x had a len method, y doesn't have it, the idea is that y should act exactly like x but time the call to the method. It's not really possible, since you add new attributes to y and there could be name collision, but if we forget about name collision, it's possible to change x in place to time a specific method, it is then later possible to put everything back in place. – Statistic Dean Aug 09 '19 at 12:08
  • I see. Well, I changed it a bit again, so now `time_method` has all the methods of the passed class. Obviously, with the exception of the special methods. So, changing in place might indeed be the best idea. Perhaps it would be good if you `deepcopy`-ed the passed instance and change the duplicate instead of the original. – Ardweaden Aug 09 '19 at 12:40
  • Well, this works for all method, I think attributes should be added as well. The only issue that you can't get around is if someone tries to access `x.__class__`, but it is not very common I guess. – Statistic Dean Aug 09 '19 at 13:31
  • Of course, I guess any other attribute types could be added in the same manner by using a different `predicate`. May I ask, because I became quite curious, what are you going to use this for? – Ardweaden Aug 09 '19 at 13:48
  • 1
    Of course, I want to time different steps of a deep learning model training in a library. Basically, I have a list of things I want to time. Those are quite easy to identify. (For example I want to time access to the dataset, so I want to time the `__getitem__` method of the dataset object), so I want to use this on all objects/method I want to time, call a fit in the library and get the times. – Statistic Dean Aug 09 '19 at 14:23