blog.humaneguitarist.org

timing stuff in Python is bad for your knees

[Wed, 16 Sep 2015 01:41:09 +0000]
A couple months ago I was shacked up recovering from a little operation. In my boredom, I watched Cheers [http://www.hulu.com/cheers] re-runs on Hulu and was dabbling with some Python stuff. I can't even remember exactly what I was doing but I wanted to time some functions. I didn't spend a lot of time looking for existing solutions for some sort of timer I could import, but I didn't like what I did see so I wrote a little one. It's a little more involved than it needs to be, but I was bored out of my mind remember? The logic got a little confusing at times, so I'm sure it's screwed up somewhere. It can record splits, and one can even use the skip() method to skip over timing certain parts of code. For example, if you make a call to a web service, maybe you don't want to time that call since its very reliant on a third party's performance. Maybe you just want to time the other stuff like what you do with the data you get from a third party web service. This is an homage to a stopwatch, hence "stopwitch.py" because a "stopwatch.py" already exists and "witch" is cooler. I'll paste the code below, but first here's an example script that uses it, followed by the output. Basically the code compares appending items to a new list vs. using a list comprehension. from stopwitch import * # Test stopwitch class by creating a new list, "new", from items in an existing list, "old". old = range(1,100000) ap = stopwitch("append") # new stopwitch instance named "append" ... lc = stopwitch("list_comprehension") lcd = stopwitch("list_comprehension_delayed") ### Appending items. ap.start() new = [] for i in old: new.append(i+1) ap.split() ### Using list comprehension. lc.start() new = [i+1 for i in old] lc.split() ### Using list comprehension and use stopwitch.skip() method to skip over superflous sleeps. import time lcd.start() new = [i+1 for i in old] lcd.skip() time.sleep(3) lcd.skip() lcd.skip() time.sleep(.3) lcd.skip() lcd.split() ### Print reports on each way of creating the new list. print ap.json() print lc.json() print lcd.json() And here's the output. The list comprehension seems to always win as it should even though this is a crappy little test. { "append": { "skipped": "0:00:00", "splits": [ { "split": "2015-10-01T10:05:20.048000", "elapsed": "0:00:00.032000" } ], "skips": [], "stop": "2015-10-01T10:05:20.048000", "elapsed": "0:00:00.032000", "start": "2015-10-01T10:05:20.016000" } } { "list_comprehension": { "skipped": "0:00:00", "splits": [ { "split": "2015-10-01T10:05:20.063000", "elapsed": "0:00:00.015000" } ], "skips": [], "stop": "2015-10-01T10:05:20.063000", "elapsed": "0:00:00.015000", "start": "2015-10-01T10:05:20.048000" } } { "list_comprehension_delayed": { "skipped": "0:00:03.323000", "splits": [ { "split": "2015-10-01T10:05:23.402000", "elapsed": "0:00:00.016000" } ], "skips": [ { "start": "2015-10-01T10:05:20.079000", "skipped": "0:00:03.011000", "stop": "2015-10-01T10:05:23.090000" }, { "start": "2015-10-01T10:05:23.090000", "skipped": "0:00:00.312000", "stop": "2015-10-01T10:05:23.402000" } ], "stop": "2015-10-01T10:05:23.402000", "elapsed": "0:00:00.016000", "start": "2015-10-01T10:05:20.063000" } } And here's the stopwitch.py script. #!/usr/bin/python """ stopwitch.py: Class for timing things like a stopwatch. """ __author__ = "Nitin Arora" import datetime import json as js import time class stopwitch(object): def __init__(self, name=None): """ Validates [name]; resets the stopwatch. """ self.name = self.__validate_str(name) self.reset() @classmethod def __validate_str(cls, string, validate=True): """ Validates that [string] is a string. """ if validate == True and string == None: return string elif isinstance(string, basestring) == False: # why "basestring"? see: https://mail.python.org/pipermail/winnipeg/2007-August/000237.html raise TypeError("Argument must be a string.") return else: return string @staticmethod def _to_iso(ss): """ Converts Unix-time/seconds to ISO 8601 format. """ return datetime.datetime.fromtimestamp(ss).isoformat() # per: http://stackoverflow.com/a/24507708 @staticmethod def _to_hms(ss): """ Converts seconds to HH:MM:SS format. """ return str(datetime.timedelta(seconds=ss)) # per: http://stackoverflow.com/a/775095 def __elapsed(self): """ Returns elapsed time between last two saved times. """ if len(self._end) < 2: elapsed = (self._end[-1] - self._begin) - self.__skipped(self._skips) else: elapsed = (self._end[-1] - self._end[-2]) - self.__skipped(self._skips) return elapsed def __skipped(self, skiplist): """ Returns time between active skip times. """ if (len(self._skips)) % 2 != 0: raise Warning("Skips must be paired.") return else: skips = skiplist skips = [(skips[x], skips[x+1]) for x in range(0,len(skips),2)] skipped = sum([float(y-x) for x,y in skips]) return skipped def reset(self): """ Resets the stopwatch. """ self._begin = time.time() self._end = [] self._skips = [] self._skiplog = [] self._splits = [] return def start(self): """ Sets the current time as the starting time. """ self._begin = time.time() return def skip(self): """ Appends the current time to the current list of active skip times. """ self._skips.append(time.time()) return def split(self): """ Sets the current time as the last saved time; records the time of the last two splits; updates log of skipped time; resets list of active skip times. """ if (len(self._skips) % 2) != 0: raise Warning("Skips must be paired; ignoring split.") else: end = time.time() self._end.append(end) self._splits.append((end, self.__elapsed())) self._skiplog += self._skips self._skips = [] return def status(self, formatted=True): """ Returns dictionary containing stopwatch usage data. """ if len(self._skips) > 0 and self._skips[0] > self._splits[-1][0]: raise Warning("Skips must be paired and followed by a split.") return if self._end == []: return {self.name:None} start = self._begin stop = self._end[-1] elapsed = sum([x[1] for x in self._splits]) splits = self._splits skips = [x for x in self._skiplog] skips = [(skips[x], skips[x+1], skips[x+1] - skips[x]) for x in range(0,len(skips),2)] skipped = self.__skipped(self._skiplog) if formatted == True: start = self._to_iso(start) stop = self._to_iso(stop) elapsed = self._to_hms(elapsed) splits = [(self._to_iso(x), self._to_hms(y)) for x,y in splits] skips = [(self._to_iso(x[0]), self._to_iso(x[1]), self._to_hms(x[2])) for x in skips] skipped = self._to_hms(skipped) skips = map(lambda x: {"start": x[0], "stop": x[1], "skipped": x[2]}, skips) splits = map(lambda x: {"split": x[0], "elapsed": x[1]}, splits) report = {self.name: {"start": start, "stop": stop, "elapsed": elapsed, "splits": splits, "skips": skips, "skipped": skipped}} return report def json(self): """ Returns JSON version of status. """ return js.dumps(self.status(), indent=2) # fin.