python лямбда-функции, определенные в al oop, в конечном итоге указывают на ту же операцию - PullRequest
0 голосов
/ 16 июня 2020

Я искал алгоритм, способный хранить и запускать функции с гибким числом аргументов. Я закончил тем, что нашел аналог переключателя / корпуса для 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

Как только я реализовал эту структуру к моему целевому коду, однако, я столкнулся со многими проблемами, потому что мои операции определены через al oop.

Насколько я понимаю, это потому, что лямбда-функции не инициализированы и в конечном итоге указывают на последнюю объявленную операцию.

Этот дополнительный код воспроизводит проблему:

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

Интересно, может ли кто-нибудь дать какой-нибудь совет, как правильно объявить лямбда-функции во втором случае или другую структуру кода, чтобы этого избежать. Большое спасибо.

Ответы [ 2 ]

1 голос
/ 20 июня 2020

Функции могут быть добавлены в словарь, используя их имена.

Когда вы отправляете вызов операции, вы можете сравнить полученные аргументы с сигнатурой соответствующей функции в реестре. Таким образом, вы могли определить другие аргументы для этой функции из необязательных аргументов, переданных в вызове диспетчеризации. Например,

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))
1 голос
/ 19 июня 2020

Проблема связана с областью видимости: каждая лямбда-функция создает «закрытие» над переменной func и может получить доступ к текущему значению этой переменной при запуске функции. И поскольку он не работает до тех пор, пока l oop не будет завершен, значение, которое он использует, является последним, которое оно хранит в l oop.

Таким образом, решение состоит в том, чтобы использовать область в ваших интересах, гарантируя что каждая лямбда закрывает переменную func, которая всегда содержит только одно значение - значение, которое вам нужно. Вам нужен новый прицел для каждой итерации l oop. И единственный способ 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)

Я бы первым признал, что это немного коряво, но должно работать. Единственное отличие от вашего кода в том, что я поместил тело l oop внутрь функции, а затем каждый раз сразу же вызывал эту функцию. Таким образом, нет никакой разницы в том, как он себя ведет, за исключением того, как переменные ограничены, что здесь является ключевым моментом. Когда эта лямбда-функция запускается, они будут использовать значение function из своей непосредственно охватывающей области, и поскольку функция add_to_dict, имеющая свою собственную область, не будет иметь собственной области, это будет другая функция каждый раз через l oop.

Я бы посоветовал вам взглянуть на эти вопросы и ответы для получения полезной информации - это примерно Javascript, а не Python, но механизмы обзора (по крайней мере, в «старой школе» Javascript до ES6) идентичны между двумя языками, и основная проблема, которая у вас здесь, идентична той, что была в этом часто задаваемом вопросе JS. К сожалению, многие решения, доступные в современном Javascript, не применимы к Python, но это применимо - простая адаптация шаблона «Immediately Invoked Function Expression» (сокращенно IIFE), распространенного в JS .

И, как вы, вероятно, можете сказать, я лично более опытен с JS, чем с Python, поэтому мне было бы интересно услышать, есть ли более приятные, более идиоматические c, Python решение этой проблемы, чем то, что я сделал выше.

...