Tuesday, 30 July 2024

Python Self-Referencing Functions

Some time ago I published a post about creating self-referencing functions in JavaScript. My approach for that was using a combination of closures and eval(). When thinking recently of having the same functionality in Python I remembered that, as I explain in this post, Python closures do not play well with exec-eval, because if we use these functions to dynamically create a new function, this function will not trap variables in the surrounding scope (the function won't be a closure trapping freevars). I was thinking about several alternatives when I realised that indeed we can use a quite more simple approach (that would also work fine in JavaScript).

It's an obvious approach once you have interiorized how methods are defined in Python and extrapolate it. They do not receive a "this" in an implicit way as in most languages, but you have to declare that "this" (that we tend to call "self" rather than "this") in the method signature. Well, let's apply a similar logic to self-referencing functions, rather than having access to themselves thanks to a variable trapped by a closure (as I was doing in JavaScript), let's just assume that the function signature has to include a parameter that gives the function access to itself. I'll call that parameter me.

I've defined a decorator function that receives a function and returns a self-referencing function. We want our self-referencing function to have access to itself, but also to allow external code to get-set attributes in the function. That way, the function has a state that is accessible not only from the function, but also from outside, which makes it more powerful than a standard closure. For that, as the decorator is returning a function that wraps the original function, I'm adding get_attr and set_attr methods to the wrapper function that will operate on the original, wrapped one. I'm using functools.wraps so that the wrapper function looks like the wrapped one (same __name__, __doc__...). OK, so this is my self_referencing decorator:


# decorator to create self-referencing functions
def self_referencing(fn: Callable) -> Callable:
    @functools.wraps(fn)
    def new_fn(*args, **kargs):
        return fn(fn, *args, **kargs)
    # I have to add these functions to the wrapper function in order to get, set attributes in the original/internal function
    new_fn.set_attr = lambda key, val: setattr(fn, key, val)
    new_fn.get_attr = lambda key: getattr(fn, key)
    return new_fn


That, given a function (e.g.: format()) we'll use like this to convert it into a self-referencing function:


@self_referencing
def format(me: Callable, txt: str):
    # me is the function itself
    print(f"{me.__name__} invoked")
    me.invoked_times = 1 if not hasattr(me, "invoked_times") else me.invoked_times + 1
    return f"{me.prefix}{txt}{me.suffix}"
	
format.set_attr("prefix", "pre_")
format.set_attr("suffix", "_post")

print(format("aa"))
print(format("bb"))

print(f"invoked {format.get_attr('invoked_times')} times")
print(f"function name: {format.__name__}")

#format invoked
#pre_aa_post
#format invoked
#pre_bb_post
#invoked 2 times
#function name: format


When using the functools.wraps decorator a __wrapped__ attribute poiting to the wrapped function is added to the wrapper function. This means that I could have skipped adding the get_attr and set_attr "methods" to the wrapper function and just use wrapper.__wrapped__.xxx, but I think it's more clear to have these 2 methods.


print(f"prefix: {format.__wrapped__.prefix}")
print(f"prefix: {format.get_attr('prefix')}")
#prefix: pre_
#prefix: pre_

No comments:

Post a Comment