2014-12-27 253 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