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

时间:2017-05-09 20:53:43

标签: python decorator python-decorators

我正在编写一个围绕REST API的通用包装器。我有几个功能,如下面的那个,负责从其电子邮件地址检索用户。感兴趣的部分是如何处理响应,基于预期状态代码列表(除了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)

process_response装饰器定义为:

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

我的问题是装饰者抱怨无法访问return_as_jsonuser_with_email_does_not_exist,因为这些回调是在内部包装的函数中定义的。

如果我决定将回调移到包装函数之外,例如与装饰器本身相同的级别,那么回调就无法访问包装函数内的响应和电子邮件变量。

# 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)

这里的正确方法是什么?我发现装饰器语法非常干净,但我无法弄清楚如何将所需的部分传递给它(回调本身或它们的输入参数,如responseemail)。

4 个答案:

答案 0 :(得分:1)

可以将装饰器键转换为字符串,然后通过f.func_code.co_consts从传递给装饰器的外部函数中拉出内部函数。不要这样做。

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

你能上课吗?如果你可以使用一个类,那么解决方案是立竿见影的。

答案 1 :(得分:1)

正如我对其他答案的评论中所提到的,这是一个使用类和装饰器的答案。这有点违反直觉,因为get_user_from_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

答案 2 :(得分:0)

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

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)

答案 3 :(得分:0)

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

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


def callback(status_code):
    def method(f):
        f.status_code = status_code
        return staticmethod(f)
    return method


def request(f):
    f.request = True
    return staticmethod(f)


def callback_redirect(cls):
    __callback_map = {}
    for attribute_name in dir(cls):
        attribute = getattr(cls, attribute_name)

        status_code = getattr(attribute, 'status_code', '')
        if status_code:
            __callback_map[status_code] = attribute

        if getattr(attribute, 'request', False):
            __request = attribute

    def call_wrapper(*args, **kwargs):
        response = __request(*args, **kwargs)
        callback = __callback_map.get(response.status_code)
        if callback is not None:
            return callback(response)
        else:
            return response.error

    return call_wrapper


@callback_redirect
class get_user_from_email:

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

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

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


print get_user_from_email('generic@email.com')
# json response: generic@email.com