salabim / ycecream

Sweeter debugging and benchmarking Python programs.
MIT License
52 stars 3 forks source link

as_str=True not returning same output #5

Closed PFython closed 3 years ago

PFython commented 3 years ago

I'm trying to catch just the 'exit' output with the ultimate goal of calculating and comparing the average run time of two functions (loop1 and loop2). When I set show_exit=True, show_enter=False the default output to screen is what I want to capture with as_str=True, but what I get is the default result y| loop(): None\n

import requests
from ycecream import y

@y(show_exit=True, show_enter=False)
def loop1():
    x = page*count
    for i in range(len(x)):
        s=len(x)

@y(show_exit=True, show_enter=False)
def loop2():
    x = page*count
    lx = len(x)
    for i in range(lx):
        s = lx

if __name__ == "__main__":
    page = requests.get('https://www.bbc.co.uk/news').text
    count = 10
    for loop in [loop1, loop2]:
        results=[]
        for run in range(10):
            result = y(loop(), as_str=True, )
            results.append(result)
        print(results)

OUTPUT

y| returned None from loop1() in 0.414763 seconds
y| returned None from loop1() in 0.418484 seconds
y| returned None from loop1() in 0.408090 seconds
y| returned None from loop1() in 0.406661 seconds
y| returned None from loop1() in 0.418872 seconds
y| returned None from loop1() in 0.413751 seconds
y| returned None from loop1() in 0.418103 seconds
y| returned None from loop1() in 0.406673 seconds
y| returned None from loop1() in 0.406783 seconds
y| returned None from loop1() in 0.412694 seconds
['y| loop(): None\n', 'y| loop(): None\n', 'y| loop(): None\n', 'y| loop(): None\n', 'y| loop(): None\n', 'y| loop(): None\n', 'y| loop(): None\n', 'y| loop(): None\n', 'y| loop(): None\n', 'y| loop(): None\n']        
y| returned None from loop2() in 0.157985 seconds
y| returned None from loop2() in 0.158627 seconds
y| returned None from loop2() in 0.160737 seconds
y| returned None from loop2() in 0.165181 seconds
y| returned None from loop2() in 0.171878 seconds
y| returned None from loop2() in 0.165958 seconds
y| returned None from loop2() in 0.164515 seconds
y| returned None from loop2() in 0.160872 seconds
y| returned None from loop2() in 0.160113 seconds
y| returned None from loop2() in 0.165789 seconds
['y| loop(): None\n', 'y| loop(): None\n', 'y| loop(): None\n', 'y| loop(): None\n', 'y| loop(): None\n', 'y| loop(): None\n', 'y| loop(): None\n', 'y| loop(): None\n', 'y| loop(): None\n', 'y| loop(): None\n']        
salabim commented 3 years ago

Peter,

I think the header of this issue is confusing, but I think I understand what you want to achieve. When you decorate the function loop1 or loop2 with @y, it will and should still return the value as returned originally from this function. And when you don't have a return, it is always None. I think changing this behaviour with some parameter would be extremely confusing.

But, there is a rather elegant way to do this type of benchmarking with ycecream, though: Instead of decorating the function put a

y.delta = 0

at the start of the function and at the end

return y.delta

So, the complete code would be

import requests
from ycecream import y

def loop1():
    y.delta=0
    x = page*count
    for i in range(len(x)):
        s=len(x)
    return y.delta

def loop2():
    y.delta=0
    x = page*count
    lx = len(x)
    for i in range(lx):
        s = lx
    return y.delta

if __name__ == "__main__":
    page = requests.get('https://www.bbc.co.uk/news').text
    count = 10
    for loop in [loop1, loop2]:
        results=[]
        for run in range(10):
            result = loop()
            results.append(result)
        print(results)

You could even consider tallying the duration in the function itself:

import requests
from ycecream import y

def loop1():
    y.delta=0
    x = page*count
    for i in range(len(x)):
        s=len(x)
    y.results.append(y.delta)

def loop2():
    global results
    y.delta=0
    x = page*count
    lx = len(x)
    for i in range(lx):
        s = lx
    y.results.append(y.delta)

if __name__ == "__main__":
    page = requests.get('https://www.bbc.co.uk/news').text
    count = 10
    for loop in [loop1, loop2]:
        y.results = []
        for run in range(10):
            loop()
        print(y.results)

Would that help?

salabim commented 3 years ago

I think there's a more elegant way to solve this problem, by redefining the output function. Then, you don't have to change the loop functions at all. Just a slightly different decorator does the job:

import requests
from ycecream import y

def collect(s):
    y.results.append(float(s[-16:-8]))

@y(output=collect, show_enter=False)
def loop1():
    x = page*count
    for i in range(len(x)):
        s=len(x)

@y(output=collect, show_enter=False)
def loop2():
    x = page*count
    lx = len(x)
    for i in range(lx):
        s = lx

if __name__ == "__main__":
    page = requests.get('https://www.bbc.co.uk/news').text
    count = 10
    for loop in [loop1, loop2]:
        y.results=[]
        for run in range(10):
            loop()
        print(y.results)

And if you would like to use this decorator even on more places without having to repeat the parameters all the time, you could fork a new ycecream instance, like:

y_benchmark = y.fork(output=collect, show_enter=False)

and then decorate with

@y_benchmark()
def loop1():
    ...

Sweet, eh?

It might look a bit strange that I use y.result to collect the data. But that prevents me from having to use another class or a global variable. Just laziness, you could say.

PFython commented 3 years ago

That really is sweet, thank Ruud. Setting output=collect is exactly the 'trick' I was looking for thanks, and you also gave me a really good example of y.fork. I've posted the final version of my code based on your suggestions here if that's of any use to anyone.