Trace and Decorators reinvented in python
Posted on August 10, 2021
Tags: codeetc
1 example
def closure():
call = 0
def fact(n):
nonlocal call
call = call + 1
if n == 1:
print(f"base case")
return 1
else:
print(f"{'->'*call} f@{n}")
ans = fact(n-1)
call = call - 1
print(f"{'<-'*call} ret{ans}")
return n* ans
fact(5)
closure()-> f@5
->-> f@4
->->-> f@3
->->->-> f@2
base case
<-<-<-<- ret1
<-<-<- ret2
<-<- ret6
<- ret241.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))
g(5)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)
g = outer(f)
print(g(5))
# (5,) Decorated!
# 5 Original!
# 4 Original!
# 3 Original!
# 2 Original!
# 1 Original!
# 15def 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
g = outer(f)
f = g #WE EXPLOITED FUNCTION MUTATION HERE!!
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 = gfpoints 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
fcallsinnerandinnercallsf
2.0.1 Closure mutation pattern
def Outer(f):
def Inner(*argv):
return f(*argv)
return Inner
f = Outer(f)
#################
# Outer(f) => Inner
def Outer(f):
def Inner(*argv):
return f(*argv) --> Outer(f)(*argv) --> Inner(argv)
return Inner2.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
f = logged(f)
# THE F**ING CLOSURE DOES THIS
g = logged(f) does nothing
#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 = "| "
trace.recursionDepth = 0
@wraps(func)
def trace_helper(*args, **kwargs):
print(f'{separate * trace.recursionDepth}|--> {func.__name__}({", ".join(map(str, args))})')
trace.recursionDepth += 1
output = func(*args,**kwargs)
trace.recursionDepth -= 1
print(f'{separate * (trace.recursionDepth + 1)}|--> return {output}')
return output
return trace_helper2.0.3 Tracing
#lang racket
(require racket/trace)
(define (fac x))
(if (< x 1)
1
(* x (fac (-x 1)))
(trace fac)
;> (fac 3)
;> > (fac 2)
;> > > (fac 1)
;< < < 1
;< < 2
;< 6