Python—变量定义的参数的确切数目

Python—变量定义的参数的确切数目,python,lambda,anonymous-function,variadic-functions,Python,Lambda,Anonymous Function,Variadic Functions,我正在创建一个方法,该方法构造一个匿名方法来返回多变量函数,例如f(x,y,z)=b。我希望用户能够传递变量列表: def get_multivar_lambda(expression, variables=["x"]) 然后,我希望返回的匿名函数完全采用len(变量)参数(基于列表索引的位置或基于列表中字符串的关键字)。我知道我可以使用*args检查长度,但这似乎不雅观 这可能吗?我该怎么做 下面是我如何对一个变量执行此操作的示例(其中seval是一个from模块simple\u eval)

我正在创建一个方法,该方法构造一个匿名方法来返回多变量函数,例如f(x,y,z)=b。我希望用户能够传递变量列表:

def get_multivar_lambda(expression, variables=["x"])
然后,我希望返回的匿名函数完全采用
len(变量)
参数(基于列表索引的位置或基于列表中字符串的关键字)。我知道我可以使用
*args
检查长度,但这似乎不雅观

这可能吗?我该怎么做

下面是我如何对一个变量执行此操作的示例(其中
seval
是一个from模块
simple\u eval
):

下面是我如何通过检查传递的
参数的长度*
完成的:

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

编辑:我从用户输入中获取表达式和变量,所以最好采用一种安全的方法

您可以将表达式解析为AST。然后可以遍历AST以计算表达式。如果您明确列出希望处理的节点类型,这是安全的

例如,使用,您可以执行以下操作

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   

我认为您不能完全按照自己的意愿(通常使用特定数量的参数定义函数)

但是simpleeval内置了变量替换:

因此,要吸取的教训是:

  • 寻找其他方法来得到你想要的
  • 调用函数时引发异常(由于解释器发现错误数量的参数)和内部引发异常之间没有太大区别

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

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

如果您可以使用Python 3,那么新引入的(Python 3.3+)将使您的代码非常干净()。这些在装饰师中也非常方便:

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

这样的事情肯定是可能的。我已经使用
ast
编写了一个解决方案。它比其他解决方案要详细一点,但是返回的对象是一个不需要任何中间编译步骤的函数,例如使用
simple\u 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
导入ast
def get_multi_lambda(expr,args=()):
代码\u stmt=ast.parse(expr,mode='eval')
收集器=名称收集器()
催收员访问(代码)
arg_set=set(args)
如果arg_set-collector.names:
raise TypeError(“未使用的参数”,参数集-收集器.名称)
elif collector.names-参数集:
#非常热心,旨在停止执行任意代码
#--防止使用不是函数参数的*any*名称
#--不幸的是,这种幼稚的方法也阻止了像sum这样的事情
raise TypeError(“尝试的非本地名称访问”,
collector.names-arg_集)
func_节点=创建函数节点(参数,代码)
code_obj=编译(函数节点,“,”评估“)
返回eval(代码_obj,{},{})
定义创建函数节点(参数、代码):
lambda_args=ast.arguments(
args=[ast.arg(name,None)表示args中的名称],
vararg=None,varargannotation=None,kwonlyargs=[],kwarg=None,
kwargannotation=None,默认值=[],kw_默认值=[]
)
func=ast.Lambda(args=Lambda\u args,body=code\u stmt.body)
expr=ast.Expression(func)
修复缺少的位置(扩展)
返回表达式
类名收集器(ast.NodeVisitor):
“”“查找ast节点树使用的所有名称。”“”
定义初始化(自):
self.names=set()
def visit_名称(自身、节点):
self.names.add(node.id)
#示例用法
func=get_multi_lambda('a/b+1',['a','b']))
打印(func(3,4))#在python 3中打印1.75

如果您可以信任这些多lambda表达式的源,或者您可以为您认为合适的某些名称添加异常,则可以选择排除“第二个名称”检查。例如,
min
max
sum
,等等。

如果将表达式作为用户的文本输入,这是危险的吗?是的,执行用户输入是危险的。有没有一种安全的方法来执行我的请求,或者这是非常危险的?为什么要默默地删除未使用的变量?这是一个失败点:我将传入一个包含未使用变量的表达式,然后传入这些变量的参数,得到一个令人困惑的错误。要么忽略未使用的变量,要么对未使用的变量进行
raise
。另外,您可以使用SymPy吗?这一点很好!我稍后会解决这个问题。可能吧,但这只是学习经验的一部分!具体地说,学习使用Python的功能我能做什么和不能做什么。我将您的循环更改为使用
zip
,因为星号在错误的位置。另外,请尝试替换操作。如何添加其他功能,如
min
max
sum
,等等?默认情况下它们都在那里。摆脱抛出异常的
elif
分支是最简单的方法。更安全的方法可能是将elif子句更改为类似以下内容:
collector.names-arg_set-set(['min','max','sum',…])
>>> 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)
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