2014-12-27 224 views
4

我正在创建一个构造匿名方法以返回多个变量的函数的方法,例如f(x,y,z)= b。我希望用户能够通过变量的列表:Python - 由变量定义的参数的确切数量

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

我则想返回的匿名函数采取准确len(variables)参数(无论是位置基于其列表索引或基于关键字的列表中的字符串)。我知道我可以使用*args并检查长度,但这看起来不够雅致。

这可能吗?我该怎么做?

这里是我是如何做的一个变量(其中seval是从模块simple_eval)为例:

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

而这里的我是如何做到的通过只是检查通过的arguments*的长度:

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 

编辑:我正在从用户输入表达和变量,所以一个安全的方法来做到这一点将是最好的。

+1

为什么静静地移除未使用的变量?这是一个失败点:我将传入一个表达式,其中包含未使用的变量,然后传递这些变量的参数,导致一个令人困惑的错误。 *忽略*未使用的变量或在未使用的变量上“提高”。 – leewz 2014-12-27 19:28:07

+0

另外,你可以使用SymPy吗? – leewz 2014-12-27 19:30:10

+0

好点!我稍后会解决这个问题。 – Langston 2014-12-27 19:30:13

回答

5

如果你可以使用Python 3,那么新引入的(Python的3.3+)inspect.Signatureinspect.Parameter可以使你的代码非常干净(PEP 362 - Function Signature Object)。这些在装饰都非常方便,以及:

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 

演示:

>>> 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 

将产生用户有用的错误信息以及:内省目的

>>> 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 

函数签名:

>>> inspect.getargspec(g) 
ArgSpec(args=['x', 'y', 'z'], varargs=None, keywords=None, defaults=None) 
1

您可以将表达式解析为AST。然后你可以通过AST来评估表达式。这可以是安全的,只要您明确列出您希望处理的节点类型即可。

例如,使用J.F. Sebastian's AST evaluator,你可以不喜欢

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 

这可以这样使用:

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

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

如果将表达式视为用户的文本输入,这会非常危险吗? – Langston 2014-12-27 19:27:09

+0

是的,执行用户输入是危险的。 – unutbu 2014-12-27 19:28:05

+0

有没有一种安全的方法可以做我想问的问题,或者这是非常重要的吗? – Langston 2014-12-27 19:28:41

1

我不认为你可以做你想要什么(通常用特定数量的参数来定义函数)。

但simpleeval有内置的变量替换:https://pypi.python.org/pypi/simpleeval#names

所以吸取的教训:

  • 另辟蹊径,以得到你想要的。
  • 调用函数时引发异常(由于解释器找到错误的参数数量)并在其中引发异常之间没有太大区别。
1

我发现使用类对象而不是标准函数应该会更好。

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 
1

这样的事情绝对有可能。我已经使用ast编写了一个解决方案。它比其他解决方案稍微冗长一些,但返回的对象是一个不需要任何中间编译步骤的函数,例如simple_eval解决方案。

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 

你可以选择排除第二名称是否可以信任这些多lambda表达式的来源,或者你可以添加你认为是精细某些名称例外。例如。 min,max,sum等...

+0

如何添加其他函数,如'min','max','sum'等? – Langston 2014-12-27 21:36:54

+0

他们在那里默认。摆脱抛出异常的'elif'分支是最简单的方法。更安全的方法可能是将elif子句更改为:'collector.names - arg_set - set(['min','max','sum',...])''。 – Dunes 2014-12-27 21:46:26