开发者

Get kwargs Inside Function

开发者 https://www.devze.com 2022-12-17 05:52 出处:网络
If I have a python function like so: def some_func(arg1, arg2, arg3=1, arg4=2): Is t开发者_JS百科here a way to determine which arguments were passed by keyword from inside the function?

If I have a python function like so:

def some_func(arg1, arg2, arg3=1, arg4=2):

Is t开发者_JS百科here a way to determine which arguments were passed by keyword from inside the function?

EDIT

For those asking why I need this, I have no real reason, it came up in a conversation and curiosity got the better of me.


No, there is no way to do it in Python code with this signature -- if you need this information, you need to change the function's signature.

If you look at the Python C API, you'll see that the actual way arguments are passed to a normal Python function is always as a tuple plus a dict -- i.e., the way that's a direct reflection of a signature of *args, **kwargs. That tuple and dict are then parsed into specific positional args and ones that are named in the signature even though they were passed by name, and the *a and **kw, if present, only take the "overflow" from that parsing, if any -- only at this point does your Python code get control, and by then the information you're requesting (how were the various args passed) is not around any more.

To get the information you requested, therefore, change the signature to *a, **kw and do your own parsing/validation -- this is going "from the egg to the omelette", i.e. a certain amount of work but certainly feasible, while what you're looking for would be going "from the omelette back to the egg"... simply not feasible;-).


Here's my solution via decorators:

def showargs(function):
    def inner(*args, **kwargs):
        return function((args, kwargs), *args, **kwargs)
    return inner

@showargs
def some_func(info, arg1, arg2, arg3=1, arg4=2):
    print arg1,arg2,arg3,arg4
    return info

In [226]: some_func(1,2,3, arg4=4)
1 2 3 4
Out[226]: ((1, 2, 3), {'arg4': 4})

There may be a way to clean this up further, but this seems minimally intrusive to me and requires no change to the calling code.

Edit: To actually test if particular args were passed by keyword, then do something like the following inside of some_func:

args, kwargs = info
if 'arg4' in kwargs:
    print "arg4 passed as keyword argument"

Disclaimer: you should probably consider whether or not you really care how the arguments were passed. This whole approach may be unnecessary.


Is there a way to determine which arguments were passed by keyword from inside the function?

In trying to assess default values of keyword parameters, yes there are options:

Code

Option 1 - locals()

def f(a, b=1, c="1"):
    print(locals())


f(0)
# {'c': '1', 'b': 1, 'a': 0}

Option 2 - Partial Type Hints*

def g(a, b:int=1, c:str="1"):
    pass


keys = g.__annotations__
values = g.__defaults__

dict(zip(keys, values))
# {'b': 1, 'c': '1'}

Option 3 - Full Type Hints*

def h(a:float, b:int=1, c:str="1") -> int:
    return 0


keys = reversed(list(filter(lambda x: x != "return", h.__annotations__)))
values = reversed(h.__defaults__)

{k: v for k, v in zip(keys, values) if k != "return"}
# {'c': '1', 'b': 1}

Note: None of these options are particularly Pythonic, but they demonstrate potential.


Details

  1. locals() depends on the function call. The results should be default values, but they change with values passed into the call, e.g. f(0) vs. f(0 2, 3)
  2. "Partial" type hints mean only keyword parameters are annotated. Adding any other annotations will not work with this naive approach.
  3. "Full" or complete type hints may include other parameters. Since a "return" annotation is optional, we filter it from our keys. Furthermore, we iterate backwards to trim potential positional parameters with zip().

*These options depend on type hints and key insertion order preservation (Python 3.6+). They only give the default values and do not change with function call values. Type hints are optional right now in Python, and thus should be used with caution in production code.


Suggestion

I would only use the latter approaches to debug or quickly inspect a function's signature. In fact, given keyword-only arguments (right of the *), one can use inspect.getargspec() to capture the kwonlydefaults dict.

def i(a, *, b=1, c="1"):
    pass


spec = inspect.getfullargspec(i)
spec
# FullArgSpec(args=['a'], varargs=None, varkw=None, 
#             defaults=None, kwonlyargs=['b', 'c'], 
#             kwonlydefaults={'b': 1, 'c': '1'}, annotations={})

spec.kwonlydefaults
# {'b': 1, 'c': '1'}

Otherwise, combine some of the mentioned techniques with the args and defaults attributes of FullArgSpec:

def get_keywords(func):
    """Return a dict of (reversed) keyword arguments from a function."""
    spec = inspect.getfullargspec(func)
    keys = reversed(spec.args)
    values = reversed(spec.defaults)
    return {k: v for k, v in zip(keys, values)}


get_keywords(f)
# {'c': '1', 'b': 1}

where the FullArgSpec from the regular function f would show:

spec = inspect.getfullargspec(f)
spec
# FullArgSpec(args=['a', 'b', 'c'], varargs=None, varkw=None, 
#             defaults=(1, '1'), kwonlyargs=[], 
#             kwonlydefaults=None, annotations={})


You're pretty much going to have to redefine your function:

def some_func(*args, **kwargs):

and do the marshaling yourself. There's no way to tell the difference between pass-by-position, pass-by-keyword, and default.


Just do it like this:

def some_func ( arg1, arg2, arg3=None, arg4=None ):
    if arg3 is None:
        arg3 = 1 # default value
    if arg4 is None:
        arg4 = 2 # default value

    # do something

That way you can see when something was set, and you are also able to work with more complex default structures (like lists) without running into problems like these:

>>> def test( arg=[] ):
        arg.append( 1 )
        print( arg )
>>> test()
[1]
>>> test()
[1, 1]


do you want to know whether arg3 was 1 because it was passed from outside or because it was a default value? No there is no way to do this as far as I'm aware. The main reason, I suspect, that there is no need for such knowledge. What typically is done is the following:

>>> def func(a, b=None):
    if b is None:
# here we know that function was called as:
# func('spam') or func('spam', None) or func('spam', b=None) or func(a='spam', b=None)

        b = 42


_kwargs decorator with inspect.signature hacks

A variant of @awesomo's solution which introduces a "private" _kwargs parameter and also works with default kwargs:

import inspect

def with_kwargs(func):
    sig = inspect.signature(func)
    def inner(*args, **kwargs):
        bound = sig.bind(*args, **kwargs)
        bound.apply_defaults()
        _kwargs = bound.arguments
        del _kwargs["_kwargs"]
        return func(*args, **kwargs, _kwargs=_kwargs)
    return inner

Usage:

@with_kwargs
def some_func(a, b, c=3, d=None, _kwargs=None):
    print(_kwargs)

>>> some_func(1, b=2, d=4)
{'a': 1, 'b': 2, 'c': 3, 'd': 4}
0

精彩评论

暂无评论...
验证码 换一张
取 消