如何检查是否设置了可选功能参数


91

在Python中,有没有一种简单的方法来检查可选参数的值是否来自其默认值,或者是因为用户已在函数调用中显式设置了它?


12
因为我想当然要检查它的功能:)
Matthias

2
只需将其None用作默认值并进行检查即可。如果您真的可以设置此测试,则还排除了用户显式传递调用默认行为的值的任何可能性。
迈克尔·巴伯

3
比起您接受的答案,至少对于CPython,这可以通过一种可重用且美观的方法来完成。请参阅下面的答案。
Ellioh

2
@Volatility:有两组默认值很重要。考虑一个递归类: Class My(): def __init__(self, _p=None, a=True, b=True, c=False) 用户使用调用它x=My(b=False)x=My(_p=self, c=True)如果函数可以检测到未显式设置b且未设置的变量将从顶级向下传递,则类方法可以调用自身。但是,如果不能,则递归调用必须显式传递每个变量:x=My(a=self.a, b=self.b, c=True, d=self.d, ...)
戴夫

@Dave但是问题是什么?以我的理解,问题是要问如何区分x=My()x=My(a=True)。您的方案涉及为可选参数分配默认值以外的其他值。
波动率

Answers:


16

很多答案都没有完整信息的一部分,因此我想将所有信息与我最喜欢的模式结合在一起。

默认值是一种mutable类型

如果默认值是可变对象,那么您很幸运:您可以利用以下事实:在定义函数时,Python的默认参数将被评估一次(上一节答案末尾的更多信息)

这意味着您可以轻松地使用is来比较默认的可变值,以查看它是作为参数传递还是默认保留,如以下示例中作为函数或方法所示:

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

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

不可变的默认参数

现在,如果您的默认值应该是一个immutable值(记住,即使字符串是不可变的!),也会有点不太优雅,因为您无法按原样利用该技巧,但是仍然可以做些事情,仍然可以利用可变的类型; 基本上,您可以在函数签名中放置一个可变的“假”默认值,并在函数主体中放置所需的“真实”默认值。

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)

如果您的实际默认值为None,那么它会特别有趣,但是它None是不可变的,因此...您仍然需要显式地使用mutable作为函数的默认参数,并在代码中切换为None。

Default类用于不可变默认值

或者类似于@cz建议,如果python docs还不够:-),则可以在两者之间添加一个对象以使API更明确(无需阅读docs);或者 used_proxy_默认类实例是可变的,并将包含您要使用的实际默认值。

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))

现在:

>>> f()
default
Default Value: 1 (<class 'int'>)
argument is: 1

>>> f(2)
passed in the call
argument is: 2

上面的也很好用Default(None)

其他图案

显然,上述模式看起来比它们应有的丑陋,因为所有print这些仅用于显示它们如何工作。否则,我会发现它们简洁而可重复。

您可以编写一个装饰器__call__,以更简化的方式添加@dmg建议的模式,但这仍然必须在函数定义本身中使用怪异的技巧-您将需要拆分,value并且value_default如果您的代码需要区分它们,那么我没有看到太多优势,因此我不会写示例:-)

可变类型作为Python中的默认值

有关#1 python陷阱的更多信息,出于您自己的喜好而受到滥用。您可以通过执行以下操作来查看定义时评估结果

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

您可以根据需要运行任意testme()多次,您将始终看到对同一默认实例的引用(因此,您的默认实例基本上是不可变的:-))。

请记住,在Python中只有3个可变内置类型setlistdict,其他一切-甚至弦乐!-是一成不变的。


您在“不可变的默认参数”中的示例实际上没有不可变的默认参数。如果确实如此,那将是行不通的。
卡罗尔

@Karol,想详细说明吗?在该示例中1,默认值是,应该是不可变的...
Stefano

我看到该函数的签名为def f(value={})
卡罗尔

@Karol是签名,但所需的默认值为1; 很抱歉,如果在解释中不清楚,但是响应的那部分要点是能够有一个不变的默认值(1)。如果您查看示例,您会看到它说:print('default'); value = 1,不是value={}
Stefano

1
哈,我明白了,谢谢。除非有人非常仔细地阅读您的文本,否则这种跟踪并不容易,这在SO上通常不会发生。考虑改写。
卡罗尔

56

并不是的。标准方法是使用不希望用户通过的默认值,例如,一个object实例:

DEFAULT = object()
def foo(param=DEFAULT):
    if param is DEFAULT:
        ...

通常,您可以将其None用作默认值,如果它对于用户想要传递的值没有意义的话。

替代方法是使用kwargs

def foo(**kwargs):
    if 'param' in kwargs:
        param = kwargs['param']
    else:
        ...

但是,这过于冗长,使您的函数更难以使用,因为其文档不会自动包含该param参数。


8
我还看到有好几个人在需要此功能的地方使用Ellipsis内置功能,没有一个被视为有效输入。这基本上与第一个示例相同。
2013年

如果要在传递了None的情况下实现特殊行为,但仍需要一种方法来测试参数是否由用户提供,则可以使用Ellipsis单例作为默认值,该单例被显式设计为跳过该值。...是的别名Ellipsis,因此想要使用位置参数的用户可以直接调用your_function(p1, ..., p3)它,从而使其变得明显且易于阅读。
Bachsau

However this is overly verbose and makes your function more difficult to use as its documentation will not automatically include the param parameter.实际上这是不正确的,因为您可以使用inspect模块设置功能及其参数的描述。它是否有效取决于您的IDE。
EZLearner

15

以下函数修饰符explicit_checker制作一组显式给出的所有参数的参数名称。它将结果作为额外的参数(explicit_params)添加到函数中。只是'a' in explicit_params检查参数a是否明确给出。

def explicit_checker(f):
    varnames = f.func_code.co_varnames
    def wrapper(*a, **kw):
        kw['explicit_params'] = set(list(varnames[:len(a)]) + 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


my_function(1)
my_function(1, 0)
my_function(1, c=1)

此代码仅在python2中有效。对于Python 3,见我的回答如下:stackoverflow.com/questions/14749328/...
R.阳

1
这很酷,但是最好首先避免更好的设计带来的问题。
卡罗尔

@Karol,我同意。在大多数情况下,如果设计合理,则无需这样做。
Ellioh

4

有时我会使用通用的唯一字符串(例如UUID)。

import uuid
DEFAULT = uuid.uuid4()
def foo(arg=DEFAULT):
  if arg is DEFAULT:
    # it was not passed in
  else:
    # it was passed in

这样,即使用户尝试了该操作,也没有用户甚至可以猜测默认值,因此我可以非常有信心,当看到的值时,该值arg没有被传入。


4
Python对象是引用,您可以使用object()代替uuid4()-它仍然是唯一的实例,可以进行is检查
cz

3

我已经看到了这个模式几次(如图书馆unittestpy-flagsjinja):

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

DEFAULT = Default()

...或等效的单线...:

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

不同于DEFAULT = object(),这有助于类型检查并在发生错误时提供信息-经常在错误消息中使用字符串表示("DEFAULT")或类名("Default")。


3

@Ellioh的答案在python 2中有效。在python 3中,以下代码应该有效:

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

此方法可以使参数名称和默认值(而不是** kwargs)具有更好的可读性。


3

您可以从foo.__defaults__和检查foo.__kwdefaults__

看下面一个简单的例子

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

然后使用运算符=is您可以比较它们

在某些情况下,下面的代码足够了

例如,您需要避免更改默认值,然后可以检查相等性,然后复制是否相等

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

同样,使用getcallargsfrom相当方便,inspect因为它返回将调用函数的实参。您将一个函数以及args和kwargs传递给该函数(inspect.getcallargs(func, /, *args, **kwds)),它将返回用于调用的真实方法的参数,同时考虑默认值和其他内容。请看下面的例子。

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


2

我同意波动率的评论。但是您可以通过以下方式进行检查:

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

我相信可选参数有点像def func(optional=value)不是**kwargs
Zaur Nasibov

这有点需要解释。带有默认值的参数和关键字参数之间的实际区别是什么?它们都使用相同的语法“ keyword = value”表示。
isedev

我不同意,因为可选参数的目的和**kwargs有所不同。PS没有关于-1的问题:)和我的-1为您是偶然的:)
Zaur Nasibov

2

这是stefano答案的一种变体,但我发现它更具可读性:

not_specified = {}

def foo(x=not_specified):
    if x is not_specified:
            print("not specified")
    else:
            print("specified")

一个投票?我最喜欢这个。简单,无需反思。可读。
森特

1

有点怪异的方法是:

class CheckerFunction(object):
    def __init__(self, function, **defaults):
        self.function = function
        self.defaults = defaults

    def __call__(self, **kwargs):
        for key in self.defaults:
            if(key in kwargs):
                if(kwargs[key] == self.defaults[key]):
                    print 'passed default'
                else:
                    print 'passed different'
            else:
                print 'not passed'
                kwargs[key] = self.defaults[key]

        return self.function(**kwargs)

def f(a):
    print a

check_f = CheckerFunction(f, a='z')
check_f(a='z')
check_f(a='b')
check_f()

哪个输出:

passed default
z
passed different
b
not passed
z

现在,正如我提到的,这很奇怪,但确实可以。但是,这非常难以理解,与ecatmur建议类似,不会自动记录在案。


1
您可能要包括的行为check_f('z'),正如您所说的那样,也很怪异。
Michael J. Barber

@ MichaelJ.Barber好点。您还必须对* args做一些“魔术”。但是,我的观点是有可能的,但是现在需要确定是否通过默认值是一个糟糕的设计。
dmg
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.