簡體   English   中英

使用裝飾器時如何要求位置 arguments?

[英]How to require positional arguments when using decorators?

我需要創建一個通用裝飾器來驗證傳遞給多個 python 函數的參數,這些函數具有相似的 arguments,但不一定以相同的順序。

python 函數是 SDK 的一部分,因此 arguments 需要是可讀的(即不能只是*args**kwargs ,因為這需要用戶通過代碼)。

讓我們考慮以下裝飾器,它強制執行a > b的約束:

from functools import wraps

def check_args(f):
    @wraps(f)
    def decorated_function(self, *args, **kwargs):
        a = kwargs["a"]
        b = kwargs["b"]
        
        if a < b:
            raise ValueError("a must be strictly greater than b")

        return f(self, *args, **kwargs)

    return decorated_function

現在考慮以下示例:

class MyClass(object):
    
    @check_args
    def f(self, *, a, b):
        return a + b

讓我們調用方法f並傳入ab作為關鍵字參數:

MyClass().f(a=2, b=1)

這按預期工作,沒有錯誤。

現在讓我們再次調用方法f ,但這次使用 arguments:

MyClass().f(1, 2)

這會引發 KeyError:

  ---------------------------------------------------------------------------
  KeyError                                  Traceback (most recent call last)
  Input In [15], in <cell line: 7>()
        3     @check_args
        4     def f(self, *, a, b):
        5         return a + b
  ----> 7 MyClass().f(1, 2)

  Input In [14], in check_args.<locals>.decorated_function(self, *args, **kwargs)
        4 @wraps(f)
        5 def decorated_function(self, *args, **kwargs):
  ----> 6     a = kwargs["a"]
        7     b = kwargs["b"]
        9     if a < b:

  KeyError: 'a'

參數現在作為args進入裝飾器,這意味着我需要將它們引用為 args[0]、args[1]。 但是,我將如何使裝飾器通用? 如果我想在不同的啟動參數的 function 上使用裝飾器怎么辦?

此外,我將*添加到 f 的 arguments 列表中,以強制用戶使用keyword arguments,但裝飾器引發了 KeyError。

如果我刪除裝飾器:

class MyClass(object):
    
    def f(self, *, a, b):
        return a + b
    
MyClass().f(2, 1)

我得到一個不同的錯誤:

  ---------------------------------------------------------------------------
  TypeError                                 Traceback (most recent call last)
  Input In [19], in <cell line: 6>()
        3     def f(self, *, a, b):
        4         return a + b
  ----> 6 MyClass().f(2, 1)

  TypeError: f() takes 1 positional argument but 3 were given

這是我想要的錯誤,因為它迫使用戶使用keyword參數!

這個問題的正確解決方案是什么? 使用裝飾器時如何強制用戶使用keyword arguments?

編輯:一個hack將檢查args列表並在此列表非空時引發錯誤。 但這聽起來像是作弊,有適當的解決方案嗎?

將args重寫為kwargs??? 也許它看起來不太好,但它正在工作,如果你真的必須......

def check_decorator(f):
    def wrapper(self,  **kwargs):
        if "a" in kwargs.keys():
            a = kwargs["a"]
        if "b" in kwargs.keys():
            b = kwargs["b"]

        if a < b:
            raise ValueError("a must be strictly greater than b")
        f(self, a, b)
    return wrapper


def change_args_decorator(fun):
    def wrapper_f(self, *args, **kwargs):
        if len(args) > 0:
            kwargs["a"] = args[0]
        if len(args) > 1:
            kwargs["b"] = args[1]
        fun(self,  **kwargs)
    return wrapper_f


class MyClass(object):
    @change_args_decorator
    @check_decorator
    def f(self,  a, b):
        print(a + b)
        return a + b


MyClass().f(2, 1)
MyClass().f(a=2, b=1)
MyClass().f(b=2, a=5)
MyClass().f(2, b=1)

3
3
7
3

嘗試檢查.getfullargspec()

from functools import wraps
import inspect

def check_args(f):
    @wraps(f)
    def decorated_function(self, *args, **kwargs):
        print(inspect.getfullargspec(f))
        a = kwargs["a"]
        b = kwargs["b"]
        
        if a < b:
            raise ValueError("a must be strictly greater than b")

        return f(self, *args, **kwargs)

    return decorated_function

class MyClass(object):
    
    @check_args
    def f(self, *, a, b):
        return a + b

MyClass().f(1, 2)

它看到了名字:

FullArgSpec(args=['self'], varargs=None, varkw=None, defaults=None, kwonlyargs=['a', 'b'], kwonlydefaults=None, annotations={})

既然ab是必需的關鍵字參數,為什么不在裝飾器中顯式標記它們呢?

from functools import wraps


def check_args(f):
    @wraps(f)
    def decorated_function(self, *args, a, b, **kwargs):
        if a < b:
            raise ValueError("a must be strictly greater than b")

        return f(self, *args, a=a, b=b, **kwargs)

    return decorated_function

測試:

In [16]: MyClass().f(2, 1)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Input In [16], in <cell line: 1>()
----> 1 MyClass().f(2, 1)

TypeError: MyClass.f() missing 2 required keyword-only arguments: 'a' and 'b'

In [17]: MyClass().f(a=2, b=1)
Out[17]: 3

我認為這個裝飾器可以完美地工作。 並允許:

  • 選擇要比較的參數
  • 選擇了需要滿足的條件
  • 如果缺少其中一個參數,則會引發錯誤,指示哪個參數

裝飾師

from functools import partial, wraps

def check_args(func=None, *, paramters_to_eval: list[str], condition=">"):
    
    if not func:
        return partial(check_args, paramters_to_eval=paramters_to_eval,
                       condition=condition)
    @wraps(func)
    def decorated_function(*args, **kwargs):
        argnames = func.__code__.co_varnames # get the argument names of the function
        if len(argnames) > 0:
            if argnames[0] == 'self':
                argnames = argnames[1:]
        
        values = dict(zip(argnames, args), **kwargs) # get arguments plus kwargs
        if set(paramters_to_eval).issubset(list(values.keys())): # only if both argument are given check conditon
            # create a condition as string to use eval
            string = f"{values[paramters_to_eval[0]]} {condition} {values[paramters_to_eval[1]]}"
            out = eval(string) # evaluate the condition                              
            if not out:
                raise ValueError(f'param "{paramters_to_eval[0]}" must be {condition} than param "{paramters_to_eval[1]}"')
        else:
            # check which params are missing and raise an error
            missing = [name for name in paramters_to_eval if name not in values.keys()]
            func_name = func.__name__
            raise Exception(f'function "{func_name}" missing parameter: "{", ".join(missing)}"')
        return func(*args, **kwargs)
    return decorated_function

現在要使用它,我們需要聲明要評估的變量是什么,並將參數名稱和條件傳遞給評估。

paramters_to_eval=['b', 'c'], condition='<' # b < c
paramters_to_eval=['c', 'b'], condition='<' # c < b

示例1

# this force b to be smaller than c always
@check_args(paramters_to_eval=['b', 'c'], condition='<')
def f1(a, b, c):
    return a + b + c

輸出

f1(1, 2, 1)
>>>
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In [162], line 1
----> 1 f1(1, 2, 1)

Cell In [147], line 20, in check_args.<locals>.decorated_function(*args, **kwargs)
     18     out = eval(string)                               
     19     if not out:
---> 20         raise ValueError(f'param "{paramters_to_eval[0]}" must be {condition} than param "{paramters_to_eval[1]}"')
     22 return func(*args, **kwargs)

ValueError: param "b" must be < than param "c"

f1(1, 2)
>>>
---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
Cell In [179], line 1
----> 1 f1(1, 2)

Cell In [177], line 26, in check_args.<locals>.decorated_function(*args, **kwargs)
     24     missing = [name for name in paramters_to_eval if name not in values.keys()]
     25     func_name = func.__name__
---> 26     raise Exception(f'function "{func_name}" missing parameter: "{", ".join(missing)}"')
     27 return func(*args, **kwargs)

Exception: function "f1" missing parameter: "c"

f1(1, 2, 4)
>>>7

示例 2

#c need to be smaller or equal to a
@check_args(paramters_to_eval=['c', 'a'], condition='<=')
def f2(a, b, c):
    return a + b + c 

輸出

f2(1, 2, 2)
>>>
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In [169], line 1
----> 1 f2(1, 2, 2)

Cell In [147], line 20, in check_args.<locals>.decorated_function(*args, **kwargs)
     18     out = eval(string)                               
     19     if not out:
---> 20         raise ValueError(f'param "{paramters_to_eval[0]}" must be {condition} than param "{paramters_to_eval[1]}"')
     22 return func(*args, **kwargs)

ValueError: param "c" must be <= than param "a"

f2(1, 2, 1)
>>> 4

Example3這個裝飾器與類完美配合

class MyClass(object):
    
    @check_args(paramters_to_eval=['a', 'b'], condition='>')
    def f(self, a, b):
        return a + b
MyClass.f(1,2)
>>>
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In [172], line 6
      3     @check_args(paramters_to_eval=['a', 'b'], condition='>')
      4     def f(self, a, b):
      5         return a + b
----> 6 MyClass.f(1,2)

Cell In [147], line 20, in check_args.<locals>.decorated_function(*args, **kwargs)
     18     out = eval(string)                               
     19     if not out:
---> 20         raise ValueError(f'param "{paramters_to_eval[0]}" must be {condition} than param "{paramters_to_eval[1]}"')
     22 return func(*args, **kwargs)

ValueError: param "a" must be > than param "b"

請注意,如果兩個參數中的任何一個沒有傳遞給 fucntion 裝飾,則裝飾器會引發錯誤,指示缺少哪個參數

暫無
暫無

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

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