4

I am creating a method that constructs an anonymous method to return a function of multiple variables e.g. f(x, y, z) = b. I want the user to be able to pass a list of variables:

def get_multivar_lambda(expression, variables=["x"])

I then want the returned anonymous function to take exactly len(variables) arguments (either positional based on their list index or keyword based on the string in the list). I know I can use *args and check the length, but this seems inelegant.

Is this possible? How might I do this?

Here is an example of how I did it for one variable (where seval is a from module simple_eval):

def get_lambda(expression, variable="x"):                                       
    return lambda arg: seval(expression.replace(variable, str(arg))) 

And here's how I did it by just checking the length of the arguments* passed:

def get_multivar_lambda(expression, variables=["x"]):

    def to_return(*arguments):
        if len(variables) != len(arguments):
            raise Exception("Number of arguments != number of variables")
        for v, a in zip(variables, arguments):
            expression.replace(v, a)
        return seval(expression)

    return to_return

EDIT: I am taking expression and variables from user input, so a safe way to do this would be best.

leewz
  • 3,201
  • 1
  • 18
  • 38
Langston
  • 1,083
  • 10
  • 26
  • 1
    Why silently remove unused variables? That's a point of failure: I will pass in an expression with unused variables, and then pass in arguments for those variables, getting a confusing error. Either *ignore* unused variables or `raise` on unused variables. – leewz Dec 27 '14 at 19:28
  • Also, can you use SymPy? – leewz Dec 27 '14 at 19:30
  • Good point! I'll fix that later on. – Langston Dec 27 '14 at 19:30
  • Probably, but part of this is just the learning experience! Specifically, learning what I can and can't do using the features of Python. – Langston Dec 27 '14 at 19:33
  • You could [parse the expression into AST](http://stackoverflow.com/a/9558001/190597). Other options include [using pyparsing](http://stackoverflow.com/a/2371789/190597), or SymPy. – unutbu Dec 27 '14 at 19:34
  • 1
    I changed your loop to use `zip` because the asterisks were in the wrong place. Also, try https://docs.python.org/3/library/stdtypes.html#str.translate for your replacement operation. – leewz Dec 27 '14 at 19:38
  • @leewangzhong Thanks, your edits are very enlightening. – Langston Dec 27 '14 at 19:39
  • @unutbu Sure, there are various ways of safely evaluating mathematical expressions. My question is more about having a variable but defined number of arguments passed to a method. Unless I'm missing what you're saying? Perhaps you could post your previous answer with more explanation to clarify? – Langston Dec 27 '14 at 19:41

5 Answers5

6

If you can use Python 3 then the newly introduced(Python 3.3+) inspect.Signature and inspect.Parameter can make your code very clean(PEP 362 - Function Signature Object). These come very handy in decorators as well:

from inspect import Parameter, signature, Signature

def get_multivar_lambda(expression, variables=["x"]):

    params = [Parameter(v, Parameter.POSITIONAL_OR_KEYWORD) for v in variables]
    sig = Signature(params)

    def to_return(*args, **kwargs):
        values = sig.bind(*args, **kwargs)
        for name, val in values.arguments.items():
            print (name, val)

    to_return.__signature__ = signature(to_return).replace(parameters=params)
    return to_return

Demo:

>>> f = get_multivar_lambda('foo')
>>> f(1)
x 1
>>> f(1, 2)
Traceback (most recent call last):
  File "<pyshell#43>", line 1, in <module>
  ...
    raise TypeError('too many positional arguments') from None
TypeError: too many positional arguments
>>> f(x=100)
x 100

Will produce useful error messages for user as well:

>>> g = get_multivar_lambda('foo', variables=['x', 'y', 'z'])
>>> g(20, 30, x=1000)
Traceback (most recent call last):
  File "<pyshell#48>", line 1, in <module>
    ....
TypeError: multiple values for argument 'x'
>>> g(1000, y=2000, z=500)
x 1000
y 2000
z 500

Function signature for introspection purpose:

>>> inspect.getargspec(g)
ArgSpec(args=['x', 'y', 'z'], varargs=None, keywords=None, defaults=None)
Ashwini Chaudhary
  • 244,495
  • 58
  • 464
  • 504
2

Something like this is definitely possible. I've written a solution using ast. It's a bit more verbose than the other solutions, but the returned object is a function that works without any intermediary compilation steps such as with the simple_eval solutions.

import ast

def get_multi_lambda(expr, args=()):
    code_stmt = ast.parse(expr, mode='eval')

    collector = NameCollector()
    collector.visit(code_stmt)

    arg_set = set(args)
    if arg_set - collector.names:
        raise TypeError("unused args", arg_set - collector.names)
    elif collector.names - arg_set:
        # very zealous, meant to stop execution of arbitrary code 
        # -- prevents use of *any* name that is not an argument to the function
        # -- unfortunately this naive approach also stops things like sum
        raise TypeError("attempted nonlocal name access", 
            collector.names - arg_set)

    func_node = create_func_node(args, code_stmt)
    code_obj = compile(func_node, "<generated>", "eval")
    return eval(code_obj, {}, {})

def create_func_node(args, code_stmt):
    lambda_args = ast.arguments(
        args=[ast.arg(name, None) for name in args],
        vararg=None, varargannotation=None, kwonlyargs=[], kwarg=None,
        kwargannotation=None, defaults=[], kw_defaults=[]
    )
    func = ast.Lambda(args=lambda_args, body=code_stmt.body)
    expr = ast.Expression(func)
    ast.fix_missing_locations(expr)
    return expr

class NameCollector(ast.NodeVisitor):
    """Finds all the names used by an ast node tree."""

    def __init__(self):
        self.names = set()

    def visit_Name(self, node):
        self.names.add(node.id)

# example usage
func = get_multi_lambda('a / b + 1', ['a', 'b'])
print(func(3, 4)) # prints 1.75 in python 3

You could choose to exclude the second names check if you can trust the source of these multi-lambda expressions, or you can add exceptions for certain names you think are fine. eg. min, max, sum, etc...

Dunes
  • 37,291
  • 7
  • 81
  • 97
  • How would one add other functions like `min`, `max`, `sum`, etc? – Langston Dec 27 '14 at 21:36
  • They are there by default. Getting rid of the `elif` branch that throws the exception is the simplest way. A safer way might be to change the elif clause to something like: `collector.names - arg_set - set(['min', 'max', 'sum', ...])`. – Dunes Dec 27 '14 at 21:46
1

You could parse the expression into AST. You can then walk through the AST to evaluate the expression. This can be safe, provided you explicitly list those node types which you wish to handle.

For example, using J.F. Sebastian's AST evaluator, you could do something like

import ast
import operator as op
import textwrap
def make_func(expression, variables):
    template = textwrap.dedent('''\
        def func({}):
            return eval_expr({!r}, locals())
        ''').format(','.join(variables), expression)
    namespace = {'eval_expr':eval_expr}
    exec template in namespace
    return namespace['func']


def eval_expr(expr, namespace):
    """
    >>> eval_expr('2^6')
    4
    >>> eval_expr('2**6')
    64
    >>> eval_expr('1 + 2*3**(4^5) / (6 + -7)')
    -5.0
    """
    # Module(body=[Expr(value=...)])
    return eval_(ast.parse(expr).body[0].value, namespace)  


def eval_(node, namespace=None):
    """
    https://stackoverflow.com/a/9558001/190597 (J.F. Sebastian)
    """
    if namespace is None:
        namespace = dict()
    if isinstance(node, ast.Num):  # <number>
        return node.n
    elif isinstance(node, ast.operator):  # <operator>
        return operators[type(node)]
    elif isinstance(node, ast.BinOp):  # <left> <operator> <right>
        return eval_(node.op, namespace)(eval_(node.left, namespace),
                                         eval_(node.right, namespace))
    elif isinstance(node, ast.Name):
        return namespace[node.id]
    else:
        raise TypeError(node)

operators = {ast.Add: op.add, ast.Sub: op.sub, ast.Mult: op.mul,
             ast.Div: op.truediv, ast.Pow: op.pow, ast.BitXor: op.xor,
             ast.USub: op.neg}

f = make_func('x', ['x'])
print(f(2))
# 2

g = make_func('x+y+z', ['x','y','z'])
print(g(1,2,3))
# 6   

Which can be used like this:

f = make_func('x', ['x'])
print(f(2))
# 2

g = make_func('x+y+z', ['x','y','z'])
print(g(1,2,3))
# 6
Community
  • 1
  • 1
unutbu
  • 842,883
  • 184
  • 1,785
  • 1,677
1

I don't think you can do exactly what you want to (generally define functions with specific numbers of arguments).

But simpleeval has variable replacement built in: https://pypi.python.org/pypi/simpleeval#names

So the lessons to be learned:

  • Look for other ways to get what you want.
  • There isn't too much of a difference between raising an exception when calling a function (due to the interpreter finding the wrong number of arguments) and raising an exception inside.
leewz
  • 3,201
  • 1
  • 18
  • 38
1

I find using a class object instead of standard function should be better.

from simpleeval import simple_eval as seval



class MultivarLambda(object):
    def __init__(self, expression, variables):
        self.__expression = expression
        self.__variables = variables


    def __call__(self, *args):
        line = self.__expression

        for v, arg in zip(self.__variables, args):
            line = line.replace(v, arg)

        return seval(line)



f = MultivarLambda("(A)**2 + (B)**2", ["A", "B"])

print f('3', '4')
print f('5', '-12')

# 25
# 169
Fomalhaut
  • 8,590
  • 8
  • 51
  • 95