3

I have written the following decorator:

def partializable(fn):
    def arg_partializer(*fixable_parameters):
        def partialized_fn(dynamic_arg):
            return fn(dynamic_arg, *fixable_parameters)

        return partialized_fn
    return arg_partializer

The purpose of this decorator is to break the function call into two calls. If I decorate the following:

@partializable
def my_fn(dyn, fix1, fix2):
    return dyn + fix1 + fix2

I then can do:

core_accepting_dynamic_argument = my_fn(my_fix_1, my_fix_2)
final_result = core_accepting_dynamic_argument(my_dyn)

My problem is that the now decorated my_fn exhibits the following signature: my_fn(*fixable_parameters)

I want it to be: my_fn(fix1, fix2)

How can I accomplish this? I probably have to use wraps or the decorator module, but I need to preserve only part of the original signature and I don't know if that's possible.

Lester Jack
  • 179
  • 8
  • I could be off base here, but if you need to initialize with certain arguments and then refer back to them repeatedly while passing in further arguments that change... it kind of sounds like you're reinventing an object. It might be cleaner to implement this as a simple class, perhaps as a [`@dataclass`](https://docs.python.org/3/library/dataclasses.html) to streamline initial value-setting. – CrazyChucky Jan 02 '21 at 21:58
  • 1
    The problem is that I have several functions in the form of my_fn, which I cannot touch, and I have to write a wrapper module that exhibits functions with the kind of signature I'm describing returning the "partialized" version. I could certainly manually write a wrapper for each with the correct signature, case by case: wrapped_my_fn(fix1, fix2) that returns a function accepting dyn1 and dyn2 that computes the result, but that would be tedious and inelegant. – Lester Jack Jan 02 '21 at 22:13
  • 1
    So you're looking to somehow automatically/programmatically distinguish which arguments are fixed and which are dynamic? Does it follow some sort of pattern? – CrazyChucky Jan 02 '21 at 22:16
  • 1
    Your question makes me realize that there would be this additional problem if both dyn and fix are variable length. I wrote it rhis way for generality. But actually the dynamic argument is always one, the first. I'm going to edit the question. – Lester Jack Jan 02 '21 at 22:19
  • Hard to tell what the actual needs are without meaningful names or clear examples, but maybe look into **functools.partial** or *memoization*? They “preset” or “cache” some args and results. – JL Peyret Jan 03 '21 at 18:04
  • @JLPeyret Unfortunately, for positional arguments, `partial` can only "preset" the earlier argument(s), whereas the asker wants to preset all but the first. And it doesn't help with preserving (most of) the function signature. I don't think memoizing/caching would be directly applicable, either, since the end results don't need to be precalculated or stored. – CrazyChucky Jan 03 '21 at 20:08
  • partial can use keyword args, not just positional. again, dont really know what you guys are trying to do, but I'd examine it thoroughly before reinventing the wheel. – JL Peyret Jan 04 '21 at 02:36

1 Answers1

3

Taking inspiration from https://stackoverflow.com/a/33112180/9204395, it's possible to accomplish this by manually altering the signature of arg_partializer, since only the signature of fn is known in the relevant scope and can be handled with inspect.

from inspect import signature

def partializable(fn):
    def arg_partializer(*fixable_parameters):
        def partialized_fn(dynamic_arg):
            return fn(dynamic_arg, *fixable_parameters)

        return partialized_fn

    # Override signature
    sig = signature(fn)
    sig = sig.replace(parameters=tuple(sig.parameters.values())[1:])
    arg_partializer.__signature__ = sig

    return arg_partializer

This is not particularly elegant, but as I think about the problem I'm starting to suspect that this (or a conceptual equivalent) is the only possible way to pull this stunt. Feel free to contradict me.

Lester Jack
  • 179
  • 8