0

Overview

At some point at run-time, I want to create a function that exactly takes a given number of arguments (known only at run-time). Exactly here means that this must not be a variadic function. Is there any way to do this without resorting to eval or similar ways to interpret strings as code?

My problem (slightly reduced)

I want to create a function that takes its arguments and passes them to another function as an iterable. Usually, I could do this as follows:

my_function = lambda *args: other_function(args)

Unfortunately, my_function will be called by some routine that cannot properly handle variadic functions¹. However, my_function will always be called with the same number of arguments n_args. So, if I knew n_args to be 3, I could use:

my_function = lambda a,b,c: other_function((a,b,c))

The problem is that I only get to know n_args at run-time, just before creating my_function. Thus, I need to generalise the above.

What I found so far

  • I achieve what I want using eval (or exec or similar):

    arg_string = ",".join( "arg_%i"%i for i in range(n_args) )
    my_function = eval(
            "lambda %s: other_function((%s))" % (arg_string,arg_string),
            {"other_function":other_function}
        )
    

    The downside to this is that I have to use eval, which is ugly and bad. If you so wish, my question is how to create something equivalent to the above without using eval or obtaining a variadic function.

  • SymPy’s lambdify allows me to dynamically create functions with a fixed number of arguments at run-time (that work in my context), but looking at the source code, it seems that it uses eval under the hood as well.


¹ It’s a compiled routine created with F2Py. I cannot change this with reasonable effort right now. Yes, it’s sad and on the long run, I will find try to fix this or get this fixed, but for now let’s accept this as given.

Wrzlprmft
  • 4,234
  • 1
  • 28
  • 54
  • Rejoice, because you can unpack and pack arguments as you please. For example, `other_function(*args)` is the same as `other_function(a, b, c)` at runtime. – cs95 Oct 21 '17 at 14:41
  • @cᴏʟᴅsᴘᴇᴇᴅ: *is the same* – Not to the routines that call my function. That’s exactly my problem. – Wrzlprmft Oct 21 '17 at 14:42
  • If you can call those routines from python, then you can use `*`, unless you're using python1. This really isn't enough to go on, and as it stands, I don't think there's another way to do this besides eval. – cs95 Oct 21 '17 at 14:43
  • @cᴏʟᴅsᴘᴇᴇᴅ: Those routines are not called from Python (but some compiled C-API code or similar) and I cannot control how they are being called. – Wrzlprmft Oct 21 '17 at 14:51
  • Surely there is a wrapper? Was this written in pyrex? cython? – cs95 Oct 21 '17 at 14:52
  • I imagine you could do something by way of AST mangling. That's less gross than `eval`... but "less gross than `eval`" doesn't say much. – Charles Duffy Oct 21 '17 at 14:55
  • @cᴏʟᴅsᴘᴇᴇᴅ: See my edit. – Wrzlprmft Oct 21 '17 at 15:07
  • I take it that the place these functions are being called aren't able to construct some sort of tuple or list themselves? – Alex Hall Oct 21 '17 at 15:56
  • @AlexHall: Correct. If I could (easily) address this, I would have done so already. – Wrzlprmft Oct 21 '17 at 16:00

1 Answers1

1

Create a function like the below. You can write a script to generate the code dynamically to make it as long as you want, but still keep the result statically in a normal python file to avoid using eval/exec.

def function_maker(n_args, other_function):
    return [
        lambda: other_function(),
        lambda arg_0: other_function(arg_0),
        lambda arg_0, arg_1: other_function(arg_0, arg_1),
    ][n_args]

Then use it as follows:

function_maker(2, other_function)(a, b)
Alex Hall
  • 34,833
  • 5
  • 57
  • 89