Thursday 10 October 2024

Partial Function Application in Python

I won't explain here the difference between partial function application and currying, there are millions of articles, posts and discussions about it everywhere. As well explained by wikipedia, partial application refers to the process of fixing a number of arguments of a function, producing another function of smaller arity. JavaScript comes with function.bind, and Python comes with functools.partial. I rarely use it though. Most times I just need to bind a parameter to a function expecting few parameters, and for that I tend to just use a lambda, I mean:


from functools import partial

def wrap_msg(wrapper: str, msg: str) -> str:
    return f"{wrapper}{msg}{wrapper}"

wrap1 = partial(wrap_msg, "||")
print(wrap1("aaa"))

wrap2 = lambda msg: wrap_msg("||", msg)
print(wrap2("aaa"))


In the above case it's a matter of taste, but for other use cases there are clear differences. First, let's see what functools.partial exactly does:

Return a new partial object which when called will behave like func called with the positional arguments args and keyword arguments keywords. If more arguments are supplied to the call, they are appended to args. If additional keyword arguments are supplied, they extend and override keywords. Roughly equivalent to:

So if we want to bind a parameter that is not the first one we'll have to bind it as a named parameter, and then we will have to invoke the resulting function passing all the parameters located after it as named ones. That's a constraint that probably makes more convenient using a lambda. On the other side, given the they extend and override keywords feature, binding a parameter as a named one gives as a very nice feature, we can use it to turn parameters into default parameters. We use partial to bind those parameters for which we've realised we want a default value, and then we still can invoke the resulting function providing those values if we want. I mean:


cities = ["Xixon", "Paris", "Lisbon"]
def join_and_wrap(items: Iterable[Any], jn: str, wr: str) -> str:
    return f"{wr}{jn.join(items)}{wr}"

join_and_wrap_with_defaults = partial(join_and_wrap, jn=".", wr="||")

# make use of both defaults
print(join_and_wrap_with_defaults(cities))

# "overwrite" one of the defaults
print(join_and_wrap_with_defaults(cities, jn=";"))
print(join_and_wrap_with_defaults(cities, wr="--"))

I was checking what others had to say about all this, and this post is an interesting one. They mention how functools.partial is pretty convenient to prevent the Closure in a loop issue. As you can see in the code below it's less verbose than the strategy of creating a wrapper function that works as scope. Notice that indeed with partial we are not creating a closure (it return a callable object that is different from a function, and traps the bound arguments as attributes of the object, not in the cells of a __closure__).


greetings = ["Bonjour", "Bon día", "Hola", "Hi"]

# remember the "closure in a loop issue" as the the variable has not a "iteration scope"

print("'- Closure in a loop' issue")
greet_fns = []
for greeting in greetings:
    def say_hi(name):
        return f"{greeting} {name}"
    greet_fns.append(say_hi)

for fn in greet_fns:
    print(fn("Francois"))

#Hi Francois
#Hi Francois
#Hi Francois
#Hi Francois

print("------------------------------")

print("- Fixed 1")
greet_fns = []
def create_scope(greeting):
    def say_hi(name):
        return f"{greeting} {name}"
    return say_hi
for greeting in greetings:
    greet_fns.append(create_scope(greeting))

for fn in greet_fns:
    print(fn("Francois"))

#Bonjour Francois
#Bon día Francois
#Hola Francois
#Hi Francois

print("------------------------------")

print("- Fixed 2")
greet_fns = []
def say_hi(greeting, name):
    return f"{greeting} {name}"
for greeting in greetings:
    greet_fns.append(partial(say_hi, greeting))

for fn in greet_fns:
    print(fn("Francois"))

#Bonjour Francois
#Bon día Francois
#Hola Francois
#Hi Francois

Reading about this stuff I've come across the better partial module, that implements an interesting approach to partial application, that feels like something in between the "classic" partial and currying. It creates partial functions that create other partial functions. You invoke them with real values and place-holders, until you have provided all the parameters. Sure this explanation is rather unclear, so I better copy-paste here an example from the module github.



from better_partial import partial, _
# Note that "_" can be imported under a different name if it clashes with your conventions

@partial
def f(a, b, c, d, e):
  return a, b, c, d, e

f(1, 2, 3, 4, 5)
f(_, 2, _, 4, _)(1, 3, 5)
f(1, _, 3, ...)(2, 4, 5)
f(..., a=1, e=5)(_, 3, _)(2, 4)
f(..., e=5)(..., d=4)(1, 2, 3)
f(1, ..., e=5)(2, ..., d=4)(3)
f(..., e=5)(..., d=4)(..., c=3)(..., b=2)(..., a=1)
f(_, _, _, _, _)(1, 2, 3, 4, 5)
f(...)(1, 2, 3, 4, 5)


No comments:

Post a Comment