python 在循环中定义的 lambda 函数,最终指向相同的操作

python lambda functions defined in a loop, end up pointing to the same operation

我一直在寻找一种能够存储 运行 具有灵活数量参数的函数的算法。我最终 finding switch/case 满足我要求的 python 类似物:

def opA_2(x, y):
    return x - y


def opB_3(x, y, z):
    return x + y - z


def opC_2(x, y):
    return x * y


def opD_3(x, y, z):
    return x * y + z


op_dict = {'opA_2': opA_2,
           'opB_3': opB_3,
           'opC_2': opC_2,
           'opD_3': opD_3
           }

op_lambda_dict = {'opA_2': lambda x, y, kwargs: op_dict['opA_2'](x, y),
                  'opB_3': lambda x, y, kwargs: op_dict['opB_3'](x, y, kwargs['z']),
                  'opC_2': lambda x, y, kwargs: op_dict['opC_2'](x, y),
                  'opD_3': lambda x, y, kwargs: op_dict['opD_3'](x, y, kwargs['z']),
                  }


def dispatch_op(func_dict, op, x, y, **kwargs):
    return func_dict.get(op, lambda a, b, c: None)(x, y, kwargs)

coefs_dict = {'i': 1, 'j': 2, 'k': 3, 'z': 4}

print('Original lambda dict result:', dispatch_op(op_lambda_dict, 'opB_3', 1, 2, **coefs_dict))

导致:

Original lambda dict result: -1

然而,一旦我将这个结构实现到我的目标代码中,我就遇到了很多问题,因为我的操作是通过循环定义的。

据我了解,这是因为 lambda 函数没有初始化,它们最终指向声明的最后一个操作。

这个额外的代码重现了这个问题:

op_looplambda_dict = {}
for label, func in op_dict.items():
    if '2' in label:
        op_looplambda_dict[label] = lambda x, y, kwargs: func(x, y)
    if '3' in label:
        op_looplambda_dict[label] = lambda x, y, kwargs: func(x, y, kwargs['z'])

print('Loop lambda dict result:', dispatch_op(op_looplambda_dict, 'opB_3', 1, 2, **coefs_dict))

导致:

Loop lambda dict result: 6

这是 opD_3 而不是 opB_3

的结果

我想知道是否有人可以就如何在第二种情况下正确声明 lambda 函数或避免它的不同代码结构提供任何建议。非常感谢。

问题与范围有关:每个 lambda 函数在 func 变量上创建一个 "closure",并且可以在函数 运行 时访问该变量的当前值。因为在循环完成之前它不会 运行ning,所以它使用的值是它在循环中保存的最后一个值。

因此,解决方案是利用作用域,确保每个 lambda 都关闭一个 func 变量,该变量只持有一个值——您需要的值。循环的每次迭代都需要一个新范围。 Python 允许您创建作用域的唯一方法是使用函数。

所以解决方案看起来像这样:

op_looplambda_dict = {}
for label, func in op_dict.items():
    def add_to_dict(function):
        if '2' in label:
            op_looplambda_dict[label] = lambda x, y, kwargs: function(x, y)
        if '3' in label:
            op_looplambda_dict[label] = lambda x, y, kwargs: function(x, y, kwargs['z'])
    add_to_dict(func)

我会第一个承认这有点笨拙,但它应该有用。与您的代码的唯一区别是我将循环体放在一个函数中 - 然后每次都立即调用该函数。所以它的行为方式没有区别,除了变量的范围——这是这里的关键。当这些 lambda 函数 运行 时,它们将使用其直接封闭作用域中 function 的值,并且由于 add_to_dict 函数具有自己的作用域,因此每个函数都是不同的函数循环时间。

我鼓励您查看 this Q&A 以获得有用的背景信息 - 它是关于 Javascript 而不是 Python,但是范围机制(至少在 "old-school" Javascript before ES6) 在两种语言之间是相同的,你在这里遇到的潜在问题与这个被问得很多的 JS 问题中的问题是相同的。不幸的是,现代 Javascript 中可用的许多解决方案不适用于 Python,但这个适用 - 对 "Immediately Invoked Function Expression"(简称 IIFE)模式的简单改编JS.

你可能会说,我个人对 JS 的经验比 Python 多,所以我很想知道是否有更好、更地道的 Python 解决方案比我上面所做的更能解决这个问题。

可以使用函数名称将函数添加到字典中。

当您调度对操作的调用时,您可以将接收到的参数与注册表中匹配函数的签名进行比较。 这样你就可以从调度调用中传递的可选参数中找出该函数的其他参数。例如,

import inspect
import functools

registry = {}

def register_operation(func):
    if func.__name__ in registry:
            raise TypeError("duplicate registration")
    func.__signature = inspect.signature(func)
    registry[func.__name__] = func
    return func

def dispatch_operation(func_name, *args, **kwargs):
    func = registry.get(func_name, None)
    if not func:
        raise TypeError("no match")
    rest_parameters = list(func.__signature.parameters)[len(args):]
    rest_args = (kwargs.get(p) for p in rest_parameters)

    ba = func.__signature.bind(*args, *rest_args)
    ba.apply_defaults()

    return func(*ba.args, **ba.kwargs)

@register_operation
def opA_2(x, y):
    return x - y

@register_operation
def opB_3(x, y, z):
    return x + y - z

@register_operation
def opC_2(x, y):
    return x * y

@register_operation
def opD_3(x, y, z):
    return x * y + z


coefs_dict = {'i': 1, 'j': 2, 'k': 3, 'z': 4}

print('Original dict result:', dispatch_operation('opB_3', 1, 2, **coefs_dict))