如何检查可选函数参数是否已设置。

120

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


12
因为我当然想在那个函数中检查它 :) - Matthias
3
只需将None用作默认值并进行检查即可。如果您真的可以设置此测试,则还应排除用户明确传递调用默认行为的值的任何可能性。 - Michael J. Barber
3
可以用一种比你接受的回答更加可重用和美观的方式来完成,至少对于CPython而言。请参见下面的我的回答。 - Ellioh
2
@Volatility:如果您有两组默认设置,则会产生影响。考虑一个递归类:Class My(): def __init__(self, _p=None, a=True, b=True, c=False)用户调用它时使用x=My(b=False)。如果函数可以检测到b未明确设置且未设置变量从顶层传递,则类方法可以使用x=My(_p=self, c=True)进行自我调用。但是,如果它们无法检测到,则递归调用必须显式传递每个变量:x=My(a=self.a, b=self.b, c=True, d=self.d, ...) - Dave
@Dave 但这是否是问题的关键?在我看来,问题是询问如何区分 x=My()x=My(a=True)。您的情况涉及将可选参数赋值为非默认值。 - Volatility
显示剩余3条评论
10个回答

68

不是真的。标准的方法是使用一个默认值,用户不需要传入这个值,例如一个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参数。


11
我也看到一些人在需要这种情况下使用省略号内置函数,而将None视为有效输入。这本质上与第一个例子相同。 - GrandOpener
如果您想在传递了 None 的情况下实现特殊行为,但仍需要一种测试用户是否提供了参数的方法,您可以使用 Ellipsis 单例作为默认值,它专门设计用作“跳过此项”的值。...Ellipsis 的别名,因此希望使用位置参数的用户只需调用 your_function(p1, ..., p3),这使得代码易于阅读和理解。 - Bachsau
然而,这样做过于冗长,使得函数更难使用,因为它的文档不会自动包含param参数。实际上,这是不正确的,因为您可以使用inspect模块设置函数及其参数的描述。它取决于您的IDE是否能够正常工作。 - EZLearner
这种类似的测试在我将Pandas Series作为参数发送时不起作用。然后对系列进行任何比较都会产生错误 “{type(self).name}的真值是模棱两可的。使用a.empty、a.bool()、a.item()、a.any()或a.all()。” 但当我使用除了Series以外的东西作为参数时,当然建议的测试函数也会失败。我找到的唯一解决方法是这个笨拙的一行代码: if not isinstance(param, pd.Series) and param==DEFAULT: - undefined
@EmilAlbert 使用is而不是== - undefined

33

很多答案只提供了部分信息,所以我想用我最喜欢的模式将它们整合在一起。

默认值是可变类型

如果默认值是可变对象,那么你很幸运:你可以利用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')

不可变的默认参数

现在,如果你的默认值预期是一个不可变的值(记住,即使字符串也是不可变的!),那么这个技巧就不太优雅了,因为你无法像之前那样利用这个技巧。但仍然有一些事情可以做,仍然可以利用可变类型;基本上,在函数签名中放置一个可变的“虚假”默认值,并将所需的“真实”默认值放在函数体中。

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是不可变的,所以...您仍然需要明确地使用一个可变作为函数默认参数,并在代码中切换到None

对于不可变的默认值,使用Default

或者,与@c-z的建议类似,如果Python文档不足够用 :-),您可以添加一个对象在其中,以使API更加明确(而无需阅读文档);使用的used_proxy_ Default类实例是可变的,并将包含您想要使用的真实默认值。

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只是为了展示它们的工作原理,上述模式看起来比它们应该更丑陋。除此之外,我认为它们足够简洁和可重复。

你可以编写一个装饰器以更流畅的方式添加@dmg建议的__call__模式,但这仍然会强制在函数定义本身中使用奇怪的技巧 - 如果您的代码需要区分valuevalue_default,则需要将它们拆分出来,因此我不认为有多大优势,并且我不会写例子 :-)

Python中可变类型作为默认值

关于#1 python gotcha!(python中容易犯的错误),上面滥用了它,更多信息请查看在定义时进行评估

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

您可以随时运行testme(),您将始终看到对同一默认实例的引用(因此基本上您的默认设置是不可变的 :-))。

请记住,在Python中只有3种可变的内置类型setlistdict; 其他所有内容 - 甚至字符串! - 都是不可变的。


你在“不可变默认参数”中的示例实际上并没有不可变的默认参数。如果有的话,它是行不通的。 - Karol
@Karol,能否详细说明一下?在该示例中的默认值为1,应该是不可变的... - Stefano
@Karol 这是函数的签名,但_期望的_默认值是1。如果在解释中没有表述清楚,请原谅,但回应部分的整个重点是为了能够拥有不可变的默认值(1)。如果您检查示例,您会看到它说:print('default'); value = 1,而不是value={} - Stefano
1
哈,现在我明白了,谢谢。除非有人仔细阅读您的文本,否则很难理解,这在SO上可能不经常发生。考虑重新措辞。 - Karol
1
在 "if default is f.defaults[0]:" 中,您必须硬编码要使用哪个默认参数编号,如果函数签名更改可能会很脆弱。另一种选择是 "if default in f.defaults:"。假设您为每个参数使用不同的Default实例,则 "in" 应该与 "is" 一样有效。 - Stephen Warren
显示剩余5条评论

16
以下函数装饰器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,请参见下面的答案:https://dev59.com/1WUq5IYBdhLWcg3wJNHg#58166804 - R. Yang
1
这很酷,但最好在第一时间通过更好的设计避免问题,如果可能的话。 - Karol
@Karol,我同意。如果设计合理,大多数情况下是不需要的。 - Ellioh

5

我曾多次看到这种模式(例如库 unittestpy-flagsjinja):

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

DEFAULT = Default()

...或等效的一行代码...

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

DEFAULT = object()不同的是,这种方法有助于类型检查并在出现错误时提供信息——通常在错误消息中使用字符串表示形式("DEFAULT")或类名("Default")。


5

有时我会使用通用唯一标识符(例如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的这个值时,它没有被传递。


6
Python对象是引用。你可以使用object()代替uuid4() - 它仍然是一个独特的实例,这就是is检查的内容。 - c z

5

@Ellioh的答案适用于Python 2。在Python 3中,下面的代码应该可以工作:

import inspect
from functools import wraps

def explicit_checker(f):
    varnames = inspect.getfullargspec(f)[0]
    @wraps(f)
    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),提高可读性。


4
你可以通过访问 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)

此外,使用inspect中的getcallargs非常方便,因为它返回函数将被调用时使用的实际参数。你只需要将函数、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


4

我赞同 Volatility 的评论。但你可以通过以下方式进行检查:

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
这是一些有点需要解释的东西。一个带有默认值的参数和一个关键字参数之间的实际区别是什么?它们都使用相同的语法“关键字=值”来表示。 - isedev
我不同意,因为可选参数和**kwargs的目的有点不同。顺便说一下,-1没问题 :) 我对你的-1是意外的 :) - Zaur Nasibov

3
这是对Stefano答案的一个变体,但我认为它更易读:
not_specified = {}

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

我进行了负投票,因为虽然这在运行时可以工作,但它会在类型方面产生问题(就像大多数其他答案一样)。 - bfontaine
请问您能详细说明一下bfontaine吗? - Kristjan Jonasson
@KristjanJonasson mypy 将此函数视为 foo(x: dict = not_specified) -> None。默认使用的虚拟值将其类型赋予参数。如果您的函数具有参数化类型,则无法正常工作:foo(x: T = not_specified); "Incompatible default for argument "x" (default has type "Dict[Any, Any]", argument has type "T")"。您可以使用 Union[T, dict],但这会使代码变得复杂。 - bfontaine
真的似乎采用Fortran的present函数的某个版本会改进Python... - Kristjan Jonasson
请注意,mypy是一个包,不是Python本身。同时,请注意类型提示是语言的可选功能。如果您为mypy工作或希望自己的代码使用并符合mypy,那么这是否会导致类型问题就很重要了。如果不是,则无关紧要。 - hlongmore
请注意,mypy是一个包,不是Python本身。同时,请注意类型提示是语言的一个可选功能。如果您在为mypy工作,或希望您自己的代码使用并符合mypy,那么这是否会导致类型方面的问题就很重要了。如果不是这样,那么就无关紧要。 - undefined

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

现在这个方法,如我所述,非常奇怪,但它能完成任务。然而,这段代码非常难读,与ecatmursuggestion类似,无法自动记录文档。

2
您可能想包括check_f('z')的行为,正如您所说的那样,这也是奇特的。 - Michael J. Barber
@MichaelJ.Barber 很好的观点。你还需要对 *args 进行一些“魔法”处理。不过,我的观点是这是可能的,但现在需要知道默认值是否被传递是一个糟糕的设计。 - dmg

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接