簡體   English   中英

如何使用Python 3.x將回調及其參數從包裝函數傳遞給裝飾器?

[英]How to pass callbacks and their arguments from wrapped function to decorator with Python 3.x?

我正在編寫一個圍繞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 )。

可以將裝飾器鍵轉換為字符串,然后通過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

你能上課嗎? 如果你可以使用一個類,解決方案是立即的。

正如我對其他答案的評論中所提到的,這是一個使用類和裝飾器的答案。 這有點違反直覺,因為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

您可以將外部函數的函數參數傳遞給處理程序:

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)

這是一種在類方法上使用函數成員數據的方法,以便將響應函數映射到適當的回調。 這對我來說似乎是最干凈的語法,但仍然有一個類變成一個函數(如果需要可以很容易地避免)。

# 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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM