簡體   English   中英

如何以與原始函數可見的相同順序處理包裝函數參數?

[英]How to process wrapped function arguments in the same order as visible for original function?

我試圖在調用之前檢查函數參數的類型(在這個例子中是foo )。 我正在使用 python 裝飾器來實現這一點。 我看不到如何以與函數foo可見的順序相同的順序獲取參數。 在下面的示例中,我得到兩個不同的排序,但具有基本相同的函數調用。

def wrapper(func):
    def f(*args, **kwargs):
        print([type(x) for x in args] + [type(v) for v in kwargs.values()])
        return func(*args, **kwargs)
    return f

@wrapper
def foo(a, b, c, d):
    print(f"{a} {b} {c} {d}")

foo(10, 12.5, 14, 5.2) # all good: int, float, int, float
foo(10, 12.5, d=5.2, c=14) # not what I want: int, float, float, int

是否有可能以一致的順序獲得論點? 如果不是,那么至少有可能讓它們全部由參數名稱鍵入嗎? 看起來像這樣的東西:

def wrapper(func):
    def f(**kwargs):
        # kwargs = {'a': 10, 'b': 12.5, 'c': 14, 'd': 5.2}
        print([type(v) for v in kwargs.values()])
        return func(*args, **kwargs)
    return f

foo(10, 12.5, 14, 5.2) # obviously doesn't work

類型檢查有點弱,只要您注釋代碼,注釋就可以工作,但是可以通過使用標准庫中的inspect來實現更健壯的方法:

它提供對框架的完全訪問,...以及您可能需要的一切。 在這種情況下, inspect.signature可用於獲取原始函數的簽名以獲取參數的原始順序。 然后只需重新組合參數並將最終組傳遞回原始函數。 更多細節在評論中。

from inspect import signature

def wrapper(func):

    def f(*args, **kwargs):
        # signature object
        sign = signature(func)

        # use order of the signature of the function as reference
        order = order = dict.fromkeys(sign.parameters)

        # update first key-values
        order.update(**kwargs)

        # update by filling with positionals
        free_pars = (k for k, v in order.items() if v is None)
        order.update(zip(free_pars, args))

        return func(**order)
    return f

@wrapper
def foo(a, b, c, d):
    print(f"{a} {b} {c} {d}")


foo(10, 12.5, 14, 5.2)
#10 12.5 14 5.2
foo(10, 12.5, d=5.2, c=14)
#10 12.5 14 5.2

該代碼與annotations兼容:

@wrapper
def foo(a: int, b: float, c: int, d: float) -> None:
    print(f"{a} {b} {c} {d}")

注釋的方式,不需要導入:

它是上述代碼的副本,但使用__annotations__屬性來獲取簽名。 請記住,注解可能有也可能沒有輸出注解

def wrapper(func):

    def f(*args, **kwargs):

        if not func.__annotations__:
            raise Exception('No clue... inspect or annotate properly')

        params = func.__annotations__

        # set return flag
        return_has_annotation = False
        if 'return' in params:
            return_has_annotation = True

        # remove possible return value
        return_ = params.pop('return', None)

        order = dict.fromkeys(params)
        order.update(**kwargs)
        free_pars = (k for k, v in order.items() if v is None)
        order.update(zip(free_pars, args))

        # update with return annotation
        if return_has_annotation:
            func.__annotations__  = params | {'return': return_}

        return func(**order)

    return f

@wrapper
def foo(a: int, b: float, c: int, d: float) -> None:
    print(f"{a} {b} {c} {d}")

首先要注意的是關鍵字參數的實現是因為順序對它們無關緊要,並且旨在在調用時按名稱將值映射到特定參數。 所以在kwargs上執行任何特定的命令沒有多大意義(或者至少會讓任何試圖使用你的裝飾器的人感到困惑)。 因此,您可能需要檢查指定了哪些kwargs並刪除相應的參數類型。

接下來,如果您希望能夠檢查參數類型,您將需要提供一種方法來告訴裝飾器您期望的類型,方法是向它傳遞一個參數(您可以在此處查看更多信息)。 這樣做的唯一方法是傳遞一個字典,將每個參數映射到預期的類型:

@wrapper({'a': int, 'b': int, c: float, d: int})
def f(a, b, c=6.0, d=5):
    pass
def wrapper(types):
    def inner(func):
        def wrapped_func(*args, **kwargs):
            # be careful here, this only works if kwargs is ordered,
            # for python < 3.6 this portion will not work
            expected_types = [v for k, v in types.items() if k not in kwargs]
            actual_types = [type(arg) for arg in args]

            # substitute these in case you are dead set on checking for key word arguments as well
            # expected_types = types
            # actual_types = [type(arg) for arg in args)] + [type(v) for v in kwargs.items]

            if expected_types != actual_types:
                raise TypeError(f"bad argument types:\n\tE: {expected_types}\n\tA: {actual_types}")

            func(*args, **kwargs)
        return wrapped_func
    return inner


@wrapper({'a': int, 'b': float, 'c': int})
def f(a, b, c):
    print('good')

f(10, 2.0, 10)
f(10, 2.0, c=10)
f(10, c=10, b=2.0)

f(10, 2.0, 10.0) # will raise exception

現在,在這一切之后,我想指出這個功能在 python 代碼中可能在很大程度上是不需要和不必要的。 Python 被設計為動態類型,因此任何類似於 Python 中的強類型的東西都違背了規律,大多數人都不會期望。

接下來,從 python 3.5 開始,我們可以訪問內置的typing包。 這使您可以指定希望在函數調用中接收的類型:

def f(a: int, b: float, c: int) -> int:
    return a + int(b) + c

現在這實際上不會為您做任何類型斷言,但它會讓您清楚地知道您期望什么類型,並且大多數(如果不是全部)IDE 會給您視覺警告,表明您將錯誤的類型傳遞給函數。

暫無
暫無

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

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