Python函数中的动态默认参数

8

我需要具有默认参数的函数,这些参数必须在函数运行时设置(例如空列表、从其他参数派生的值或从数据库中获取的数据),目前我正在使用以下模式来处理此类情况:

def foo(bar, baz=None):
    baz = baz if baz else blar()
    # Stuff

blar() 函数提供了 baz 的默认值,这个值在执行过程中可能会发生变化。然而,baz = baz if baz else ... 这一行代码让我感觉不够优雅。是否有其他更好的方法来避免函数参数默认值只绑定一次的问题呢?可以使用可通过 pip 安装的小型跨平台库来替代。

5个回答

12

不,就是这样。通常你测试是否为 None ,这样你就可以安全地传入类似于0""等假值了。

def foo(bar, baz=None):
    baz = baz if baz is not None else blar()

老派的方式是两行代码。有些人可能更喜欢这种方式。

def foo(bar, baz=None):
    if baz is None:
        baz = blar()

6
baz 不为 None 时,这种老式的方法避免了不必要的重新分配。 - chepner

7
你可以替换

标签。

baz = baz if baz else blar()

使用

baz = baz or blar()

如果您仍然愿意只测试虚值而不是None,那么这样做也没问题。

0
你可以像这样做:
def getArg():
  try:
      return arg
  except NameError:
      return 0

def foo(x, y=getArg):
    y = y()
    print(y)

foo(1) # Prints 0 (Default)

arg = 7 # Set by argparse?

foo(2)           # Prints 7 (Dynamic global)
foo(3, lambda:9) # Prints 9 (Dynamic passed)

0
一个快速而简单的示例实现,可能会起作用:
class DynDefault(object):
    def __init__(self, callback):
        self.callback = callback
    def __call__(self):
        return self.callback()

def dyn_default(func):
    def wrapper(*args, **kw):
        args = [arg() for arg in args if isinstance(arg, DynDefault) else arg]
        for k, v in kw.items():
            if isinstance(v, DynDefault):
                kw[k] = v()
        return func(*args, **kw)
    return wrapper

@dyn_default
def foo(bar, baaz=DynDefault(blar)):
    # problem solved

这个解决方案有问题。通过处理args,您没有惰性评估动态默认参数,而是在实际调用函数时提供任何可能的DynDefault。 一个实际的解决方案可以是用描述符对象替换foo.func_defaults,但是func_defaults只能是元组。 虽然子类化元组,并重载__iter__和__getitem__可以让我们继续进行,但对CPython源代码的一些分析表明,func_defaults对象的成员直接在PyObject中访问内存,而不是通过新样式类接口访问,因此这无法解决。 :( - Vajk Hermecz

0
pip install dynamic-default-args

这与其他黑客技巧有些相似,只是更加优雅。 其想法是创建一个容器类来存储动态默认参数,并使用内省来获取装饰函数的签名,然后为其生成专用包装器。 例如,对于此函数:

from dynamic_default_args import dynamic_default_args, named_default

@dynamic_default_args(format_doc=True)
def foo(a, b=named_default(name='b', value=5),
        /,
        c=named_default(name='c', value=object),
        *d,
        e=1e-3, f=named_default(name='f', value='will it work?'),
        **g):
    """ A function with dynamic default arguments.
    Args:
        a: Required Positional-only argument a.
        b: Positional-only argument b. Dynamically defaults to {b}.
        c: Positional-or-keyword argument c. Dynamically defaults to {c}.
        *d: Varargs.
        e: Keyword-only argument e. Defaults to 1e-3.
        f: Keyword-only argument f. Dynamically defaults to {f}
        **g: Varkeywords.
    """
    print(f'Called with: a={a}, b={b}, c={c}, d={d}, e={e}, f={f}, g={g}')

正如您所知,Python有5种参数,根据它们相对于语法中的/***的位置进行分类:

def f(po0, ..., /, pok0, ..., *args, kw0, kw1, ..., **kwargs):
      ----------   --------    |     --------------    |
      |            |           |     |                 |
      |            Positional- |     |             Varkeywords
      |            or-keyword  |     Keyword-only
      Positional-only        Varargs  

我们生成一个字符串表达式expr,其中包含包装函数的定义,并根据其类型调用原始函数的参数遵循上述规则。它的内容应该类似于这样:
def wrapper(a, b=b_, c=c_, *d, e=e_, f=f_, **g):
    return func(a,
                b.value if isinstance(b, named_default) else b,
                c.value if isinstance(c, named_default) else c,
                *d,
                e=e,
                f=f.value if isinstance(f, named_default) else f,
                **g)

接下来,使用包含默认参数b_、c_、e_、f_的上下文字典,从foo的签名中获取,将expr编译,函数为func=foo,并使用我们定义的类named_default

exec_locals = {}
exec(compile(expr, '<foo_wrapper>', 'exec'), context, exec_locals)
wrapper = functools.wraps(func)(exec_locals[wrapper_alias])

所有这些都在开始时执行(不是惰性初始化),因此我们可以在运行时限制一个额外的函数调用,并且对于每个函数,类型检查和属性访问开销的最小量(比调用另一个函数检索默认值要高效得多)。
容器的值以后可以被修改,函数的文档字符串也将自动重新格式化。
named_default('b').value += 10
named_default('f').value = 'it works'
help(foo)
# foo(a, b=15, /, c=<class 'object'>, *d, e=0.001, f='it works!', **g)
#     A function with dynamic default arguments.
#     Args:
#         a: Required Positional-only argument a.
#         b: Positional-only argument b. Dynamically defaults to 6.
#         c: Positional-or-keyword argument c. Dynamically defaults to <class'object'>.
#         *d: Varargs.
#         e: Keyword-only argument e. Defaults to 1e-3.
#         f: Keyword-only argument f. Dynamically defaults to it works!
#         **g: Varkeywords.

动态修改foo.__defaults__也可以完成任务并且更加高效。

了解更多:dynamic-default-args


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