Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Having trouble following the return of a recursive Python function with exceptions

Tags:

python

I have a decorator called traced that draws lines that connect function calls to their recursive return values.

class traced(object):
    __nested=0      
    def __init__(self,f):
        self.__f=f
        self.__name__=f.__name__
    def __call__(self,*args,**dargs):
        l=0
        arguments=""
        for arg in args:
            if(l!=0):
                arguments+=", "
            arguments +=str(arg)
            l=l+1
        items=dargs.iteritems()
        for i in items:
            if(l!=0):
                arguments+=", "
            arguments +=(str(i[0])+" = "+str(i[1]))
            l=l+1
        print("| "*type(self).__nested),
        print(",- "+self.__name__+"("+arguments+")")
        type(self).__nested+=1
        try:
            rv = self.__f(*args,**dargs)
            type(self).__nested+=-1
            print("| "*type(self).__nested),
            print("`- "+repr(rv))
            return rv

Here is an example of it running with this function:

@traced
def foo(a, b):
   if a == 0:
       return b
   return foo(b=a-1, a=b-1)

foo(4 ,5)
,- foo(4, 5)
| ,- foo(a=4, b=3)
| | ,- foo(a=2, b=3)
| | | ,- foo(a=2, b=1)
| | | | ,- foo(a=0, b=1)
| | | | `- 1
| | | `- 1
| | `- 1
| `- 1
`- 1
1

I need it to run correctly with functions that raise exceptions, and a function is provided that I need it to be able to run with, but I'm not sure how to edit my decorator because I can't follow the function's output. Here is the function:

class ChangeException(Exception):
    pass

@traced
def change_t(l,a):
    if a==0:
        return []
    elif len(l)==0:
        raise ChangeException()
    elif l[0]>a:
        return change_t(l[1:],a)
    else:
        try:
            return [l[0]]+change_t(l,a-l[0])
        except ChangeException:
            return change_t(l[1:],a)

and the provided output when calling change_t([9, 7, 5], 44):

,- change_t([9, 7, 5], 44)
| ,- change_t([9, 7, 5], 35)
| | ,- change_t([9, 7, 5], 26)
| | | ,- change_t([9, 7, 5], 17)
| | | | ,- change_t([9, 7, 5], 8)
| | | | | ,- change_t([7, 5], 8)
| | | | | | ,- change_t([7, 5], 1)
| | | | | | | ,- change_t([5], 1)
| | | | | | | | ,- change_t([], 1)
| | | | | | ,- change_t([5], 8)
| | | | | | | ,- change_t([5], 3)
| | | | | | | | ,- change_t([], 3)
| | | | | | | ,- change_t([], 8)
| | | | ,- change_t([7, 5], 17)
| | | | | ,- change_t([7, 5], 10)
| | | | | | ,- change_t([7, 5], 3)
| | | | | | | ,- change_t([5], 3)
| | | | | | | | ,- change_t([], 3)
| | | | | | ,- change_t([5], 10)
| | | | | | | ,- change_t([5], 5)
| | | | | | | | ,- change_t([5], 0)
| | | | | | | | `- []
| | | | | | | `- [5]
| | | | | | `- [5, 5]
| | | | | `- [5, 5]
| | | | `- [7, 5, 5]
| | | `- [7, 5, 5]
| | `- [9, 7, 5, 5]
| `- [9, 9, 7, 5, 5]
`- [9, 9, 9, 7, 5, 5]
[9, 9, 9, 7, 5, 5]

My current output:

 ,- change_t([9, 7, 5], 44)
|  ,- change_t([9, 7, 5], 35)
| |  ,- change_t([9, 7, 5], 26)
| | |  ,- change_t([9, 7, 5], 17)
| | | |  ,- change_t([9, 7, 5], 8)
| | | | |  ,- change_t([7, 5], 8)
| | | | | |  ,- change_t([7, 5], 1)
| | | | | | |  ,- change_t([5], 1)
| | | | | | | |  ,- change_t([], 1)
| | | | | | | | |  ,- change_t([5], 8)
| | | | | | | | | |  ,- change_t([5], 3)
| | | | | | | | | | |  ,- change_t([], 3)
| | | | | | | | | | | |  ,- change_t([], 8)
| | | | | | | | | | | | |  ,- change_t([7, 5], 17)
| | | | | | | | | | | | | |  ,- change_t([7, 5], 10)
| | | | | | | | | | | | | | |  ,- change_t([7, 5], 3)
| | | | | | | | | | | | | | | |  ,- change_t([5], 3)
| | | | | | | | | | | | | | | | |  ,- change_t([], 3)
| | | | | | | | | | | | | | | | | |  ,- change_t([5], 10)
| | | | | | | | | | | | | | | | | | |  ,- change_t([5], 5)
| | | | | | | | | | | | | | | | | | | |  ,- change_t([5], 0)
| | | | | | | | | | | | | | | | | | | |  `- []
| | | | | | | | | | | | | | | | | | |  `- [5]
| | | | | | | | | | | | | | | | | |  `- [5, 5]
| | | | | | | | | | | | | | | | |  `- [5, 5]
| | | | | | | | | | | | | | | |  `- [7, 5, 5]
| | | | | | | | | | | | | | |  `- [7, 5, 5]
| | | | | | | | | | | | | |  `- [9, 7, 5, 5]
| | | | | | | | | | | | |  `- [9, 9, 7, 5, 5]
| | | | | | | | | | | |  `- [9, 9, 9, 7, 5, 5]
[9, 9, 9, 7, 5, 5]

I understand how change_t gets to this change_t([], 3), but don't understand why the next call is change_t([], 8).

How would I change my decorator to handle exceptions correctly?

like image 963
Anatoliy Sokolov Avatar asked May 31 '26 13:05

Anatoliy Sokolov


1 Answers

This is an old question, and the code as shown doesn't run properly on Python 3, but I was intrigued enough to figure it out. I've updated the code to work, and while I was at it I also tried to clean it up and make it a little easier to read (including use of modern features like f-strings). Functionally (pun intended), this should be entirely equivalent:

class traced:
    def __init__(self, function):
        self._function = function
        self.__name__ = function.__name__
        self._nest_level = 0
    
    def __call__(self, *args, **kwargs):
        arguments = ", ".join(
            [str(arg) for arg in args]
            + [f"{k}={v}" for k, v in kwargs.items()]
        )

        print(f"{self._indentation},- {self.__name__}({arguments})")
        self._nest_level += 1
        
        try:
            return_value = self._function(*args, **kwargs)
            self._nest_level -= 1
            print(f"{self._indentation}`- {return_value}")
            return return_value

        except Exception:
            raise

    @property
    def _indentation(self):
        return "| " * self._nest_level

As noted in the comments, the except block is missing from the code in the question. I've filled it in with what, as far as I can tell, must have been there in order to produce the output shown. (Not entirely sure why the exception would be caught and immediately rethrown; perhaps there was a suggestion that there should be a try/except block, and the asker didn't know what to put in it yet.) This accurately reproduces:

>>> change_t([9, 7, 5], 44)
 ,- change_t([9, 7, 5], 44)
|  ,- change_t([9, 7, 5], 35)
| |  ,- change_t([9, 7, 5], 26)
| | |  ,- change_t([9, 7, 5], 17)
| | | |  ,- change_t([9, 7, 5], 8)
| | | | |  ,- change_t([7, 5], 8)
| | | | | |  ,- change_t([7, 5], 1)
| | | | | | |  ,- change_t([5], 1)
| | | | | | | |  ,- change_t([], 1)
| | | | | | | | |  ,- change_t([5], 8)
| | | | | | | | | |  ,- change_t([5], 3)
| | | | | | | | | | |  ,- change_t([], 3)
| | | | | | | | | | | |  ,- change_t([], 8)
| | | | | | | | | | | | |  ,- change_t([7, 5], 17)
| | | | | | | | | | | | | |  ,- change_t([7, 5], 10)
| | | | | | | | | | | | | | |  ,- change_t([7, 5], 3)
| | | | | | | | | | | | | | | |  ,- change_t([5], 3)
| | | | | | | | | | | | | | | | |  ,- change_t([], 3)
| | | | | | | | | | | | | | | | | |  ,- change_t([5], 10)
| | | | | | | | | | | | | | | | | | |  ,- change_t([5], 5)
| | | | | | | | | | | | | | | | | | | |  ,- change_t([5], 0)
| | | | | | | | | | | | | | | | | | | |  `- []
| | | | | | | | | | | | | | | | | | |  `- [5]
| | | | | | | | | | | | | | | | | |  `- [5, 5]
| | | | | | | | | | | | | | | | |  `- [5, 5]
| | | | | | | | | | | | | | | |  `- [7, 5, 5]
| | | | | | | | | | | | | | |  `- [7, 5, 5]
| | | | | | | | | | | | | |  `- [9, 7, 5, 5]
| | | | | | | | | | | | |  `- [9, 9, 7, 5, 5]
| | | | | | | | | | | |  `- [9, 9, 9, 7, 5, 5]
[9, 9, 9, 7, 5, 5]

All the function calls and return values are correct; the only issue is that the indentation keeps going. The way to fix this is to reduce nesting by one whenever the exception is caught:

class traced:
    def __init__(self, function):
        self._function = function
        self.__name__ = function.__name__
        self._nest_level = 0
    
    def __call__(self, *args, **kwargs):
        arguments = ", ".join(
            [str(arg) for arg in args]
            + [f"{k}={v}" for k, v in kwargs.items()]
        )

        print(f"{self._indentation},- {self.__name__}({arguments})")
        self._nest_level += 1
        
        try:
            return_value = self._function(*args, **kwargs)
            self._nest_level -= 1
            print(f"{self._indentation}`- {return_value}")
            return return_value

        except Exception:
            self._nest_level -= 1 # This is the missing piece
            raise

    @property
    def _indentation(self):
        return "| " * self._nest_level

This correctly outputs:

>>> change_t([9, 7, 5], 44)
,- change_t([9, 7, 5], 44)
| ,- change_t([9, 7, 5], 35)
| | ,- change_t([9, 7, 5], 26)
| | | ,- change_t([9, 7, 5], 17)
| | | | ,- change_t([9, 7, 5], 8)
| | | | | ,- change_t([7, 5], 8)
| | | | | | ,- change_t([7, 5], 1)
| | | | | | | ,- change_t([5], 1)
| | | | | | | | ,- change_t([], 1)
| | | | | | ,- change_t([5], 8)
| | | | | | | ,- change_t([5], 3)
| | | | | | | | ,- change_t([], 3)
| | | | | | | ,- change_t([], 8)
| | | | ,- change_t([7, 5], 17)
| | | | | ,- change_t([7, 5], 10)
| | | | | | ,- change_t([7, 5], 3)
| | | | | | | ,- change_t([5], 3)
| | | | | | | | ,- change_t([], 3)
| | | | | | ,- change_t([5], 10)
| | | | | | | ,- change_t([5], 5)
| | | | | | | | ,- change_t([5], 0)
| | | | | | | | `- []
| | | | | | | `- [5]
| | | | | | `- [5, 5]
| | | | | `- [5, 5]
| | | | `- [7, 5, 5]
| | | `- [7, 5, 5]
| | `- [9, 7, 5, 5]
| `- [9, 9, 7, 5, 5]
`- [9, 9, 9, 7, 5, 5]
[9, 9, 9, 7, 5, 5]

Broad except clauses are usually a bad idea, but I think this is an exception (pun intended again). This way the decorator is general purpose and doesn't need to know which specific exception(s) the recursive function intends to throw to itself. The decorator will always re-raise any exception it's given, so we shouldn't have to worry about masking problems. (Still a good idea to use except Exception rather than a bare except!)


As to the other half of the question:

I understand how change_t gets to this change_t([], 3), but don't understand why the next call is change_t([], 8)

Let's jump back a couple of levels and take it one step at a time. Here's the relevant part of the output, with some spacing added to make it a little easier to see:

,- change_t([5], 8)
|  
|  ,- change_t([5], 3)
|  |  
|  |  ,- change_t([], 3)
|  
|  ,- change_t([], 8)

And this is what's happening:

The function is called with inputs l=[5], a=8. Let's call this our level 1. The first element of l is less than a, so it subtracts and calls...

Level 2: [5], 3. It can't subtract, so it drops the first list element and calls:

Level 3: [], 3. This raises ChangeException.

We're back at level 2. We've been thrown an exception, but in the first elif, where we're not prepared to catch it, so it's passed along.

Now we're back to level 1. We were given an exception in the try block, so we go to except. Remember that at this level, l is still [5] and a is still 8. Within except, we call the function again with all but the first element of l (so, []) and a as-is (8). This is why the next call is [], 8.

like image 57
CrazyChucky Avatar answered Jun 03 '26 02:06

CrazyChucky