[英]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
並傳入a
和b
作為關鍵字參數:
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={})
既然a
和b
是必需的關鍵字參數,為什么不在裝飾器中顯式標記它們呢?
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.