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
<- 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))
g(5)

However notice it is impossible to record which stackframe we are on.

2 Global scope and handbuilt decorator

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!
# 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

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

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 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
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_helper

2.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