How to check whether optional function parameter is set

前端 未结 10 1349
傲寒
傲寒 2020-12-02 15:06

Is there an easy way in Python to check whether the value of an optional parameter comes from its default value, or because the user has set it explicitly at the function ca

相关标签:
10条回答
  • 2020-12-02 15:28

    @Ellioh's answer works in python 2. In python 3, the following code should work:

    import inspect
    def explicit_checker(f):
        varnames = inspect.getfullargspec(f)[0]
        def wrapper(*a, **kw):
            kw['explicit_params'] = set(list(varnames[:len(a)]) + list(kw.keys()))
            return f(*a, **kw)
        return wrapper
    
    @explicit_checker
    def my_function(a, b=0, c=1, explicit_params=None):
        print a, b, c, explicit_params
        if 'b' in explicit_params:
            pass # Do whatever you want
    

    This method can keep the argument names and default values (instead of **kwargs) with better readability.

    0 讨论(0)
  • 2020-12-02 15:28

    I agree with Volatility's comment. But you could check in the following manner:

    def function(arg1,...,**optional):
        if 'optional_arg' in optional:
            # user has set 'optional_arg'
        else:
            # user has not set 'optional_arg'
            optional['optional_arg'] = optional_arg_default_value # set default
    
    0 讨论(0)
  • 2020-12-02 15:32

    Lot of answers have little pieces of the full info, so I'd like to bring it all together with my favourite pattern(s).

    default value is a mutable type

    If the default value is a mutable object, you are lucky: you can exploit the fact that Python’s default arguments are evaluated once when the function is defined (some more about this at the end of the answer in the last section)

    This means you can easily compare a default mutable value using is to see if it was passed as an argument or left by default, as in the following examples as function or method:

    def f(value={}):
        if value is f.__defaults__[0]:
            print('default')
        else:
            print('passed in the call')
    

    and

    class A:
        def f(self, value={}):
            if value is self.f.__defaults__[0]:
                print('default')
            else:
                print('passed in the call')
    

    Immutable default arguments

    Now, it's a bit less elegant if your default is expected to be an immutable value (and remember that even strings are immutable!) because you can't exploit the trick as it is, but there is still something you can do, still exploiting mutable type; basically you put a mutable "fake" default in the function signature, and the desired "real" default value in the function body.

    def f(value={}):
        """
        my function
        :param value: value for my function; default is 1
        """
        if value is f.__defaults__[0]:
            print('default')
            value = 1
        else:
            print('passed in the call')
        # whatever I want to do with the value
        print(value)
    

    It feels particularly funny if you real default is None, but None is immutable so... you still need to explicitly use a mutable as the function default parameter, and switch to None in the code.

    Using a Default class for immutable defaults

    or, similar to @c-z suggestion, if python docs are not enough :-) , you can add an object in between to make the API more explicit (without reading the docs); the used_proxy_ Default class instance is mutable, and will contain the real default value you want to use.

    class Default:
        def __repr__(self):
            return "Default Value: {} ({})".format(self.value, type(self.value))
    
        def __init__(self, value):
            self.value = value
    
    def f(default=Default(1)):
        if default is f.__defaults__[0]:
            print('default')
            print(default)
            default = default.value
        else:
            print('passed in the call')
        print("argument is: {}".format(default))
    

    now:

    >>> f()
    default
    Default Value: 1 (<class 'int'>)
    argument is: 1
    
    >>> f(2)
    passed in the call
    argument is: 2
    

    The above works nicely also for Default(None).

    Other patterns

    Obviously the above patterns looks uglier than they should because of all the print which are there only for showing how they work. Otherwise I find them terse and repeatable enough.

    You could write a decorator to add the __call__ pattern suggested by @dmg in a more streamlined way, but this will still oblige to use weird tricks in the function definition itself - you would need to split out value and value_default if your code need to distinguish them, so I don't see much advantage and I won't write the example :-)

    Mutable types as default values in Python

    A bit more about #1 python gotcha!, abused for your own pleasure above. You can see what happens due to the evaluation at definition by doing:

    def testme(default=[]):
        print(id(default))
    

    You can run testme() as many time as you want, you will always see a reference to the same default instance (so basically your default is immutable :-) ).

    Remember that in Python there are only 3 mutable built-in types: set, list, dict; everything else - even strings! - is immutable.

    0 讨论(0)
  • 2020-12-02 15:34

    You can check it from foo.__defaults__ and foo.__kwdefaults__

    see a simple example bellow

    def foo(a, b, c=123, d=456, *, e=789, f=100):
        print(foo.__defaults__)
        # (123, 456) 
        print(foo.__kwdefaults__)
        # {'e': 789, 'f': 100}
        print(a, b, c, d, e, f)
    
    #and these variables are also accessible out of function body
    print(foo.__defaults__)    
    # (123, 456)  
    print(foo.__kwdefaults__)  
    # {'e': 789, 'f': 100}
    
    foo.__kwdefaults__['e'] = 100500
    
    foo(1, 2) 
    #(123, 456)
    #{'f': 100, 'e': 100500}
    #1 2 123 456 100500 100
    

    then by using operator = and is you can compare them

    and for some cases code bellow is enought

    For example, you need to avoid changing default value then you can check on equality and then copy if so

        def update_and_show(data=Example):
            if data is Example:
                data = copy.deepcopy(data)
            update_inplace(data) #some operation
            print(data)
    

    Also, it is quite convenient to use getcallargs from inspect as it returns real arguments with which function will be invoked. You pass a function and args and kwargs to it (inspect.getcallargs(func, /, *args, **kwds)), it will return real method's arguments used for invocation, taking into consideration default values and other stuff. Have a look at an example below.

    from inspect import getcallargs
    
    # we have a function with such signature
    def show_params(first, second, third=3):
        pass
    
    # if you wanted to invoke it with such params (you could get them from a decorator as example)
    args = [1, 2, 5]
    kwargs = {}
    print(getcallargs(show_params, *args, **kwargs))
    #{'first': 1, 'second': 2, 'third': 5}
    
    # here we didn't specify value for d
    args = [1, 2, 3, 4]
    kwargs = {}
    
    # ----------------------------------------------------------
    # but d has default value =7
    def show_params1(first, *second, d = 7):
        pass
    
    
    print(getcallargs(show_params1, *args, **kwargs))
    # it will consider b to be equal to default value 7 as it is in real method invocation
    # {'first': 1, 'second': (2, 3, 4), 'd': 7}
    
    # ----------------------------------------------------------
    args = [1]
    kwargs = {"d": 4}
    
    def show_params2(first, d=3):
        pass
    
    
    print(getcallargs(show_params2, *args, **kwargs))
    #{'first': 1, 'd': 4}
    

    https://docs.python.org/3/library/inspect.html

    0 讨论(0)
  • 2020-12-02 15:40

    I've seen this pattern a few times (e.g. library unittest, py-flags, jinja):

    class Default:
        def __repr__( self ):
            return "DEFAULT"
    
    DEFAULT = Default()
    

    ...or the equivalent one-liner...:

    DEFAULT = type( 'Default', (), { '__repr__': lambda x: 'DEFAULT' } )()
    

    Unlike DEFAULT = object(), this assists type-checking and provides information when errors occur -- frequently either the string representation ("DEFAULT") or the class name ("Default") are used in error messages.

    0 讨论(0)
  • 2020-12-02 15:40

    This is a variation on stefano's answer, but i find a little more readable:

    not_specified = {}
    
    def foo(x=not_specified):
        if x is not_specified:
                print("not specified")
        else:
                print("specified")
    
    0 讨论(0)
提交回复
热议问题