Trace and Decorators reinvented in python
Posted on August 10, 2021
Tags: codeetc
1 example
def closure():
= 0
call def fact(n):
nonlocal call
= call + 1
call if n == 1:
print(f"base case")
return 1
else:
print(f"{'->'*call} f@{n}")
= fact(n-1)
ans = call - 1
call print(f"{'<-'*call} ret{ans}")
return n* ans
5)
fact(
closure()
-> f@5
->-> f@4
->->-> f@3
->->->-> f@2
base case
<-<-<-<- ret1
<-<-<- ret2
<-<- ret6
<- ret24
1.1 Attempt
def f(n):
print(n, 'Original!')
if n == 1: return(1)
else: return(g(n - 1) + n)
def g(n):
print(n, "Decorated")
return(f(n))
5) g(
However notice it is impossible to record which stackframe we are on.
2 Global scope and handbuilt decorator
- It is possible to emulate this with global scope and closures.
def outer(F):
def inner(*argv):
print(argv, 'Decorated!')
return(F(*argv))
return(inner)
def f(n):
print(n, 'Original!')
if n == 1: return(1)
else: return(f(n - 1) + n)
= outer(f)
g print(g(5))
# (5,) Decorated!
# 5 Original!
# 4 Original!
# 3 Original!
# 2 Original!
# 1 Original!
# 15
def outer(F):
def inner(*argv):
print(argv, 'Decorated!')
return(F(*argv))
return(inner)
def f(n): #THIS IS MUTATED AFTER f = g
print(n, 'Original!')
if n == 1: return(1)
else: return(f(n - 1) + n) #THIS IS MUTATED AFTER f = g
= outer(f)
g = g #WE EXPLOITED FUNCTION MUTATION HERE!!
f print(f(5))
# (5,) Decorated!
# 5 Original!
# (4,) Decorated!
# 4 Original!
# (3,) Decorated!
# 3 Original!
# (2,) Decorated!
# 2 Original!
# (1,) Decorated!
# 1 Original!
# 15
- the reason for this behavior is after assignment
f = g
f
points toinner(f)
F(*arg)
still points to the originalf
- Weirdly this means the only way to call the original f is through inner.
- The result is a mutual recursive behavior where
f
callsinner
andinner
callsf
2.0.1 Closure mutation pattern
def Outer(f):
def Inner(*argv):
return f(*argv)
return Inner
= Outer(f)
f #################
# Outer(f) => Inner
def Outer(f):
def Inner(*argv):
return f(*argv) --> Outer(f)(*argv) --> Inner(argv)
return Inner
2.0.2 Decorators Mutate the target function
Decorators are impure by default.
def logged(func):
def with_logging(*args, **kwargs):
print(func.__name__ + " was called")
return func(*args, **kwargs)
return with_logging
#################
@logged
def f(x):
return x
# # # the decorator above is the same as doing the below
def f(x):
return x
= logged(f)
f # THE F**ING CLOSURE DOES THIS
= logged(f) does nothing
g #logged(f) returns a function with_logging
#with_logging gets stored in f
print(f.__name__)
#> with_logging
##################
2.0.2.0.1 functool wraps to prevent sideeffect
def trace(func):
= "| "
separate = 0
trace.recursionDepth
@wraps(func)
def trace_helper(*args, **kwargs):
print(f'{separate * trace.recursionDepth}|--> {func.__name__}({", ".join(map(str, args))})')
+= 1
trace.recursionDepth = func(*args,**kwargs)
output -= 1
trace.recursionDepth print(f'{separate * (trace.recursionDepth + 1)}|--> return {output}')
return output
return trace_helper
2.0.3 Tracing
#lang racketrequire racket/trace)
(
(define (fac x))if (< x 1)
(1
* x (fac (-x 1)))
(trace fac)
(
;> (fac 3)
;> > (fac 2)
;> > > (fac 1)
;< < < 1
;< < 2
;< 6