簡體   English   中英

Python 將 args 轉換為 kwargs

[英]Python convert args to kwargs

我正在編寫一個裝飾器,它需要在調用它正在裝飾的函數之前調用其他函數。 裝飾函數可能有位置參數,但裝飾器將調用的函數只能接受關鍵字參數。 有沒有人有將位置參數轉換為關鍵字參數的方便方法?

我知道我可以獲得修飾函數的變量名稱列表:

>>> def a(one, two=2):
...    pass

>>> a.func_code.co_varnames
('one', 'two')

但是我不知道如何分辨什么是位置傳遞的,什么是關鍵字。

我的裝飾器看起來像這樣:

class mydec(object):
    def __init__(self, f, *args, **kwargs):
        self.f = f

    def __call__(self, *args, **kwargs):
        hozer(**kwargs)
        self.f(*args, **kwargs)

除了比較 kwargs 和 co_varnames 之外,還有什么方法可以向 kwargs 添加任何不在那里的東西,並希望得到最好的結果?

任何按位置傳遞的 arg 都將傳遞給 *args。 任何作為關鍵字傳遞的 arg 都將傳遞給 **kwargs。 如果您有位置參數值和名稱,則可以執行以下操作:

kwargs.update(dict(zip(myfunc.func_code.co_varnames, args)))

將它們全部轉換為關鍵字參數。

如果您使用 Python >= 2.7 inspect.getcallargs()為您開箱即用。 您只需將裝飾函數作為第一個參數傳遞給它,然后將其余參數完全按照您計划調用它的方式傳遞。 例子:

>>> def f(p1, p2, k1=None, k2=None, **kwargs):
...     pass
>>> from inspect import getcallargs

我打算做f('p1', 'p2', 'p3', k2='k2', extra='kx1') (注意 k1 是作為 p3 在位置上傳遞的),所以......

>>> call_args = getcallargs(f, 'p1', 'p2', 'p3', k2='k2', extra='kx1')
>>> call_args
{'p2': 'p2', 'k2': 'k2', 'k1': 'p3', 'p1': 'p1', 'kwargs': {'extra': 'kx1'}}

如果您知道裝飾函數不會使用**kwargs ,那么該鍵將不會出現在 dict 中,您就完成了(我假設沒有*args ,因為這會打破一切都具有的要求一個名字)。 如果你確實**kwargs ,就像我在這個例子中那樣,並且想要將它們與其余的命名參數一起包含,則需要多一行:

>>> call_args.update(call_args.pop('kwargs'))
>>> call_args
{'p2': 'p2', 'k2': 'k2', 'k1': 'p3', 'p1': 'p1', 'extra': 'kx1'}

更新:對於 Python >= 3.3,請參閱inspect.Signature.bind()和相關的inspect.signature函數以了解類似於(但比) inspect.getcallargs()

注意 - co_varnames 將包括局部變量和關鍵字。 這可能無關緊要,因為 zip 會截斷較短的序列,但如果您傳遞了錯誤數量的 args,則可能會導致混淆錯誤消息。

您可以使用func_code.co_varnames[:func_code.co_argcount]避免這種情況,但最好使用檢查模塊。 IE:

import inspect
argnames, varargs, kwargs, defaults = inspect.getargspec(func)

您可能還想處理函數定義**kwargs*args (即使只是在與裝飾器一起使用時引發異常)。 如果設置了這些,則getargspec的第二個和第三個結果將返回它們的變量名稱,否則它們將為 None。

嗯,這可能有點矯枉過正。 我是為 dectools 包(在 PyPi 上)編寫的,所以你可以在那里獲得更新。 它返回考慮位置、關鍵字和默認參數的字典。 包中有一個測試套件(test_dict_as_called.py):

def _dict_as_called(function, args, kwargs):
    """ return a dict of all the args and kwargs as the keywords they would
    be received in a real function call.  It does not call function.
    """

    names, args_name, kwargs_name, defaults = inspect.getargspec(function)
    
    # assign basic args
    params = {}
    if args_name:
        basic_arg_count = len(names)
        params.update(zip(names[:], args))  # zip stops at shorter sequence
        params[args_name] = args[basic_arg_count:]
    else:
        params.update(zip(names, args))    
    
    # assign kwargs given
    if kwargs_name:
        params[kwargs_name] = {}
        for kw, value in kwargs.iteritems():
            if kw in names:
                params[kw] = value
            else:
                params[kwargs_name][kw] = value
    else:
        params.update(kwargs)
    
    # assign defaults
    if defaults:
        for pos, value in enumerate(defaults):
            if names[-len(defaults) + pos] not in params:
                params[names[-len(defaults) + pos]] = value
            
    # check we did it correctly.  Each param and only params are set
    assert set(params.iterkeys()) == (set(names)|set([args_name])|set([kwargs_name])
                                      )-set([None])
    
    return params

Nadia 的答案是正確的,但我覺得該答案的工作演示很有用。

def decorator(func):
    def wrapped_func(*args, **kwargs):
        kwargs.update(zip(func.__code__.co_varnames, args))
        print(kwargs)
        return func(**kwargs)
    return wrapped_func

@decorator
def thing(a,b):
    return a+b

鑒於此修飾函數,以下調用將返回適當的答案:

thing(1, 2)  # prints {'a': 1, 'b': 2}  returns 3
thing(1, b=2)  # prints {'b': 2, 'a': 1}  returns 3
thing(a=1, b=2)  # prints {'a': 1, 'b': 2}  returns 3

但是請注意,如果您開始嵌套裝飾器,事情會變得很奇怪,因為裝飾函數現在不再需要 a 和 b,而是需要 args 和 kwargs:

@decorator
@decorator
def thing(a,b):
    return a+b

這里thing(1,2)將打印{'args': 1, 'kwargs': 2}TypeError: thing() got an unexpected keyword argument 'args'錯誤TypeError: thing() got an unexpected keyword argument 'args'

這是使用inspect.signature (適用於Python 3.3+)解決此問題的較新方法。 我先舉一個可以自己運行/測試的例子,然后展示如何用它修改原始代碼。

這是一個測試函數,它總結了給它的任何 args/kwargs; 至少需要一個參數 ( a ) 並且有一個帶有默認值 ( b ) 的僅關鍵字參數,只是為了測試函數簽名的不同方面。

def silly_sum(a, *args, b=1, **kwargs):
    return a + b + sum(args) + sum(kwargs.values())

現在讓我們為silly_sum制作一個包裝器,它可以以與silly_sum相同的方式silly_sum (除了我們將要silly_sum一個例外),但它只會將 kwargs 傳遞給包裝后的silly_sum

def wrapper(f):
    sig = inspect.signature(f)
    def wrapped(*args, **kwargs):
        bound_args = sig.bind(*args, **kwargs)
        bound_args.apply_defaults()
        print(bound_args) # just for testing

        all_kwargs = bound_args.arguments
        assert len(all_kwargs.pop("args", [])) == 0
        all_kwargs.update(all_kwargs.pop("kwargs"))
        return f(**all_kwargs)
    return wrapped

sig.bind返回一個BoundArguments對象,但這不會考慮默認值,除非您顯式調用apply_defaults 如果沒有給出*args / **kwargs ,這樣做也會為 args 生成一個空元組,並為 kwargs 生成一個空字典。

sum_wrapped = wrapper(silly_sum)
sum_wrapped(1, c=9, d=11)
# prints <BoundArguments (a=1, args=(), b=1, kwargs={'c': 9, 'd': 11})>
# returns 22

然后我們只需獲取參數字典並添加任何**kwargs 。使用此包裝器的例外是*args不能傳遞給函數。 這是因為這些沒有名稱,所以我們不能將它們轉換為 kwargs。 如果將它們作為名為 args 的 kwarg 傳遞是可以接受的,那么可以改為這樣做。


以下是如何將其應用於原始代碼:

import inspect


class mydec(object):
    def __init__(self, f, *args, **kwargs):
        self.f = f
        self._f_sig = inspect.signature(f)

    def __call__(self, *args, **kwargs):
        bound_args = self._f_sig.bind(*args, **kwargs)
        bound_args.apply_defaults()
        all_kwargs = bound_args.arguments
        assert len(all_kwargs.pop("args"), []) == 0
        all_kwargs.update(all_kwargs.pop("kwargs"))
        hozer(**all_kwargs)
        self.f(*args, **kwargs)

@mikenerone 提出的(最佳)解決方案是原始海報問題的解決方案:

import inspect
from functools import wraps

class mydec(object):
    def __init__(self, f, *args, **kwargs):
        self.f = f

    def __call__(self, *args, **kwargs):
        call_args = inspect.getcallargs(self.f, *args, **kwargs)
        hozer(**call_args)

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

def hozer(**kwargs):
    print('hozer got kwds:', kwargs)

def myadd(i, j=0):
    return i + j

o = mydec(myadd)
assert o(1,2) == 3
assert o(1) == 1
assert o(1, j=2) == 3
hozer got kwds: {'i': 1, 'j': 2}
hozer got kwds: {'i': 1, 'j': 0}
hozer got kwds: {'i': 1, 'j': 2}

這是一個通用裝飾器,它將 Python 函數的所有參數轉換並合並為kwargs並僅使用這些 kwargs 調用包裝函數。

def call_via_kwargs(f):
    @wraps(f)
    def wrapper(*args, **kwds):
        call_args = inspect.getcallargs(f, *args, **kwds)
        print('kwargs:', call_args)
        return f(**call_args)
    return wrapper


@call_via_kwargs
def adder(i, j=0):
    return i + j

assert adder(1) == 1
assert adder(i=1) == 1
assert adder(1, j=2) == 3
kwargs: {'i': 1, 'j': 0}
kwargs: {'i': 1, 'j': 0}
kwargs: {'i': 1, 'j': 2}

這些解決方案正確處理默認參數。

暫無
暫無

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

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