如何使用Python3.x将回调函数及其参数从包装函数传递到装饰器?

如何使用Python3.x将回调函数及其参数从包装函数传递到装饰器?,python,decorator,python-decorators,Python,Decorator,Python Decorators,我正在围绕RESTAPI编写一个通用包装器。我有几个类似下面的功能,负责从用户的电子邮件地址检索用户。感兴趣的部分是如何根据预期状态代码列表(除了HTTP200)和与每个预期状态代码相关联的回调处理响应: import requests def get_user_from_email(email): response = requests.get('http://example.com/api/v1/users/email:%s' % email) # define call

我正在围绕RESTAPI编写一个通用包装器。我有几个类似下面的功能,负责从用户的电子邮件地址检索用户。感兴趣的部分是如何根据预期状态代码列表(除了HTTP
200
)和与每个预期状态代码相关联的回调处理响应:

import requests

def get_user_from_email(email):
    response = requests.get('http://example.com/api/v1/users/email:%s' % email)

    # define callbacks
    def return_as_json(response):
        print('Found user with email [%s].' % email)
        return response.json()

    def user_with_email_does_not_exist(response):
        print('Could not find any user with email [%s]. Returning `None`.' % email),
        return None

    expected_status_codes_and_callbacks = {
        requests.codes.ok: return_as_json,  # HTTP 200 == success
        404: user_with_email_does_not_exist,
    }
    if response.status_code in expected_status_codes_and_callbacks:
        callback = expected_status_codes_and_callbacks[response.status_code]
        return callback(response)
    else:
        response.raise_for_status()


john_doe = get_user_from_email('john.doe@company.com')
print(john_doe is not None)  # True

unregistered_user = get_user_from_email('unregistered.user@company.com')
print(unregistered_user is None)  # True
上面的代码工作得很好,所以我想重构并概括响应处理部分。我希望以以下代码结束:

@process_response({requests.codes.ok: return_as_json, 404: user_with_email_does_not_exist})
def get_user_from_email(email):
    # define callbacks
    def return_as_json(response):
        print('Found user with email [%s].' % email)
        return response.json()

    def user_with_email_does_not_exist(response):
        print('Could not find any user with email [%s]. Returning `None`.' % email),
        return None

    return requests.get('https://example.com/api/v1/users/email:%s' % email)
过程\u响应
装饰器定义为:

import functools

def process_response(extra_response_codes_and_callbacks=None):

    def actual_decorator(f):

        @functools.wraps(f)
        def wrapper(*args, **kwargs):
            response = f(*args, **kwargs)

            if response.status_code in expected_status_codes_and_callbacks:
                action_to_perform = expected_status_codes_and_callbacks[response.status_code]
                return action_to_perform(response)
            else:
                response.raise_for_status()  # raise exception on unexpected status code

        return wrapper

    return actual_decorator
我的问题是,decorator抱怨无法访问
return\u,因为\u json
user\u with\u email\u不存在,因为这些回调是在包装函数中定义的

# does not work either, as response and email are not visible from the callbacks
def return_as_json(response):
    print('Found user with email [%s].' % email)
    return response.json()

def user_with_email_does_not_exist(response):
    print('Could not find any user with email [%s]. Returning `None`.' % email),
    return None

@process_response({requests.codes.ok: return_as_json, 404: user_with_email_does_not_exist})
def get_user_from_email(email):
    return requests.get('https://example.com/api/v1/users/email:%s' % email)
如果我决定将回调移到包装函数之外,例如与装饰器本身处于同一级别,那么回调将无法访问包装函数内的响应和电子邮件变量

# does not work either, as response and email are not visible from the callbacks
def return_as_json(response):
    print('Found user with email [%s].' % email)
    return response.json()

def user_with_email_does_not_exist(response):
    print('Could not find any user with email [%s]. Returning `None`.' % email),
    return None

@process_response({requests.codes.ok: return_as_json, 404: user_with_email_does_not_exist})
def get_user_from_email(email):
    return requests.get('https://example.com/api/v1/users/email:%s' % email)

这里的正确方法是什么?我发现decorator语法非常清晰,但我无法确定如何将所需的部分传递给它(回调本身或它们的输入参数,如
response
email
)。

可以将外部函数的函数参数传递给处理程序:

def return_as_json(response, email=None):  # email param
    print('Found user with email [%s].' % email)
    return response.json()

@process_response({requests.codes.ok: return_as_json, 404: ...})
def get_user_from_email(email):
    return requests.get('...: %s' % email)

# in decorator
     # email param will be passed to return_as_json
     return action_to_perform(response, *args, **kwargs)
您可以将decorator键转换为字符串,然后通过
f.func\u code.co\u consts
从传递给decorator的外部函数中提取内部函数。不要这样做

import functools, new
from types import CodeType

def decorator(callback_dict=None):

    def actual_decorator(f):
        code_dict = {c.co_name: c for c in f.func_code.co_consts if type(c) is CodeType}

        @functools.wraps(f)
        def wrapper(*args, **kwargs):
            main_return = f(*args, **kwargs)

            if main_return['callback'] in callback_dict:
                callback_string = callback_dict[main_return['callback']]
                callback = new.function(code_dict[callback_string], {})
                return callback(main_return)

        return wrapper

    return actual_decorator


@decorator({'key_a': 'function_a'})
def main_function(callback):

    def function_a(callback_object):
        for k, v in callback_object.items():
            if k != 'callback':
                print '{}: {}'.format(k, v)

    return {'callback': callback, 'key_1': 'value_1', 'key_2': 'value_2'}


main_function('key_a')
# key_1: value_1
# key_2: value_2

你会使用课堂吗?如果您可以使用类,那么解决方案是即时的。

如我的另一个答案的注释中所述,下面是一个使用类和装饰器的答案。这有点违反直觉,因为
get\u user\u from\u email
被声明为一个类,但在修饰后作为一个函数结束。但是,它确实具有所需的语法,因此这是一个加号。也许这是一个清洁解决方案的起点

# dummy response object
from collections import namedtuple
Response = namedtuple('Response', 'data status_code error')

def callback_mapper(callback_map):

    def actual_function(cls):

        def wrapper(*args, **kwargs):
            request = getattr(cls, 'request')
            response = request(*args, **kwargs)

            callback_name = callback_map.get(response.status_code)
            if callback_name is not None:
                callback_function = getattr(cls, callback_name)
                return callback_function(response)

            else:
                return response.error

        return wrapper

    return actual_function


@callback_mapper({'200': 'json', '404': 'does_not_exist'})
class get_user_from_email:

    @staticmethod
    def json(response):
        return 'json response: {}'.format(response.data)

    @staticmethod
    def does_not_exist(response):
        return 'does not exist'

    @staticmethod
    def request(email):
        response = Response('response data', '200', 'exception')
        return response

print get_user_from_email('blah')
# json response: response data

下面是一种使用类方法上的函数成员数据将响应函数映射到适当回调的方法。对我来说,这似乎是最干净的语法,但仍然有一个类变成了一个函数(如果需要,可以很容易地避免)


您可以将
*args、**kwargs
传递到
操作中以执行
。如果您假设嵌套在请求函数中的响应处理程序非常重要,即超过几行,那么您将看到您想要的“路由器”与嵌套的不可重用处理程序相比,向成功处理程序或失败处理程序进行调度实际上没有多大意义。在此基础上进行重构之前,我会先编写更多的代码。@jornsharpe谢谢,请参阅下面我对schwobaseggl@JLPeyret我确实写了更多的代码。涵盖了所有API端点,从而产生了25个以上具有完全相同的体系结构和响应处理的包装函数。因此,我希望用装饰器重构所有这些。你是说用类来装饰器部分?是的,我可以使用类,没问题。我没有研究这条路径,因为我认为这两种方法提供的特性是相同的(类与标准函数)。据我所知,类将帮助存储状态。这是你想要的吗?然后,在Python3.x中使用闭包和
非局部
关键字可能会获得相同的结果,如果主函数是类,回调是方法,那么通过字符串值调用这些方法就像
getattr(cls,method\u string)
一样简单。同样,您仍然需要将字符串传递给decorator,但不必进行可怕的函数检查。我马上写一个例子。在这种情况下,类会很好,因为它是一个对象,而不是因为它保留状态。我曾经这样做过,但没有发现语法非常优雅。下面是我为了使回调签名尽可能通用而想到的<代码>电子邮件
不是唯一可能的参数。def return_as_json(response,*args,**kwargs):email=kwargs.get('email')或args[1]#假设我知道原始调用打印中电子邮件参数的索引('Found user with email[%s].%email)return response.json()谢谢@Jared Goguen。这并不像我想的那么直接,但我还是喜欢它。听起来确实是一个好的开始。@LaurentVaylet我想用
@回调
装饰器检查一下最后一个解决方案。对我来说,这似乎是最优雅的结构。你说得对。装饰器更复杂,但最终结果可能更好。