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?
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_tgets to thischange_t([], 3), but don't understand why the next call ischange_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 oflis less thana, 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 raisesChangeException.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
tryblock, so we go toexcept. Remember that at this level,lis still[5]andais still8. Withinexcept, we call the function again with all but the first element ofl(so,[]) andaas-is (8). This is why the next call is[], 8.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With