如何确定缺失的Python关键字参数是哪个?

5

当忘记向函数传递某些参数时,Python会给出一个不太有帮助的消息“myfunction()需要X个参数(已给出Y个)”。是否有一种方法可以找出缺少的参数名称并告诉用户?类似于:

try:
    #begin blackbox
    def f(x,y):
        return x*y

    f(x=1)
    #end blackbox
except Exception as e:
    #figure out the missing keyword argument is called "y" and tell the user so

假设在 "begin blackbox" 和 "end blackbox" 之间的代码对异常处理程序来说是未知的。 编辑:正如下面有人指出的那样,Python 3 已经内置了这个功能。那么让我扩展一下问题,是否有一种(可能丑陋和笨拙的)方法在 Python 2.x 中实现这个功能?

1
@CrazyCasta 这并没有真正回答问题... - poke
3
这是一个有用且相关的问题,但不是重复的。现在人们在 SO 上关闭投票时太容易冲动了... - wim
1
这是哪个版本的Python?对于早期的2.x、后期的2.x、早期的3.x和后期的3.x,答案是不同的... - abarnert
1
请注意,当前的Python 3版本将为您提供此信息。升级吧! :) - wim
1
如果这只是一个练习的话,那么现在可能是开始尝试使用tracebacks、frames、code objects、inspect.getsource、dis.dis等工具的好时机。但实际上,我认为你最好先转向Python 3.4,因为在那里,所有这些都比2.7更容易、更干净、更有趣,而且你所获得的知识将会更有价值。 - abarnert
显示剩余13条评论
4个回答

3
一种更加简洁的方法是将函数包装在另一个函数中,通过*args, **kwargs传递,并在需要时使用这些值,而不是在事后重构它们。但如果你不想这样做...
在Python 3.x(除了非常早期的版本),这很容易,就像poke的答案所解释的那样。在3.3+版本中更加容易,使用inspect.signatureinspect.getargvaluesinspect.Signature.bind_partial等工具。
在Python 2.x中,没有办法做到这一点。异常只有字符串'f() takes exactly 2 arguments (1 given)'在其args中。
但在特定的CPython 2.x中,可以通过足够丑陋和脆弱的技巧实现。
你有一个回溯,因此你有它的tb_frametb_lineno...这就是你需要的一切。只要源代码可用,inspect模块就可以轻松获取实际的函数调用表达式。然后你只需要解析它(通过ast),获取传递的参数,并与函数的签名进行比较(不幸的是,在2.x中并不像在3.3+中那么容易获取,但在f.func_defaultsf.func_code.co_argcount等之间,你可以重构它)。
但如果源代码不可用呢?嗯,在tb_frame.f_codetb_lasti之间,你可以找到函数调用在字节码中的位置。而dis模块使这相对容易解析。特别是,在函数调用之前,位置参数和关键字参数的名称-值对都被推送到堆栈上,因此你可以轻松地看到哪些名称被推送,并且有多少个位置值,并通过这种方式重构函数调用。这样你就可以以相同的方式与签名进行比较了。
当然,这依赖于CPython编译器如何构建字节码的一些假设。只要堆栈最终得到正确的值,就可以以各种不同的顺序执行操作。所以,它相当脆弱。但我认为已经有更好的理由不这样做了。

@wim:我原本计划为此编写一些示例代码……但后来我决定,如果有人是“作为练习”在做这个,那么他从头开始基于链接构建它会更有趣——看看半文档化的code对象中有什么和没有什么,ast对象和/或反汇编看起来像什么等等。所以我重写了整个东西,让它变得更晚了。 - abarnert

2
except TypeError as e:
   import inspect
   got_args = int(re.search("\d+.*(\d+)",str(e)).groups()[0])
   print "missing args:",inspect.getargspec(f).args[got_args:]

一个更好的方法是使用装饰器。
def arg_decorator(fn):
   def func(*args,**kwargs):
       try: 
           return fn(*args,**kwargs)
       except TypeError:
           arg_spec = inspect.getargspec(fn)
           missing_named = [a for a in arg_spec.args if a not in kwargs]
           if arg_spec.defaults:
                    missing_args = missing_named[len(args): -len(arg_spec.defaults) ]
           else:
                    missing_args = missing_named[len(args):]
           print "Missing:",missing_args
   return func

@arg_decorator
def fn1(x,y,z):                      
      pass

def fn2(x,y):
    pass

arged_fn2 = arg_decorator(fn2)

fn1(5,y=2)
arged_fn2(x=1)

2
异常消息中的数字并不会告诉您缺少哪个参数,只会告诉您缺少了多少个参数。 - poke
尝试使用 f(y=5) 查看输出,因此从技术上讲,是的,它只是有多少...你可以使用它来获取最后一个索引。 - Joran Beasley
f(y=5) 仍然给我返回 "missing args: ['y']",我是不是漏掉了什么? - marius
当我在2.6中执行它时,我得到了x,y,但即使我使用第二个位置命名参数调用它,它仍返回“aa()需要两个非关键字参数(0个给定)”。 - Joran Beasley
-1 这根本不起作用。尝试使用 f(y=1),它仍然会告诉你缺少 y。 - wim
显示剩余3条评论

2

我认为这样做并没有太多意义。这种异常是由于程序员未指定参数而引起的。因此,如果您故意捕获异常,那么您可以在第一时间修复它。

话虽如此,在当前的Python 3版本中,抛出的TypeError确实会提到哪些参数在调用中缺失:

"f() missing 1 required positional argument: 'y'"

很不幸,参数名称没有单独提及,因此您需要从字符串中提取它:

try:
    f(x=1)
except TypeError as e:
    if 'required positional argument' in e.args[0]:
        argumentNames = e.args[0].split("'")[1::2]
        print('Missing arguments are ' + argumentNames)
    else:
        raise # Re-raise other TypeErrors

正如Joran Beasley在评论中指出的那样,Python 2不会告诉你缺少哪些参数,只会告诉你缺少了多少个参数。因此,在异常中无法确定调用时缺少了哪些参数。

Python 2 中的 IndexError - wim
@wim 我在2.6中遇到了TypeError。 - Joran Beasley
@wim,是针对哪一行的? - poke
这是在Python 3.1+中执行此操作的最简单和最佳方法(我想...也许是3.2+)。它不适用于2.x或3.0,但是在那里没有任何简单的方法可行。 - abarnert
我明白了,我之前使用的是2.7版本。我不知道Python 3会自动告诉你名称。 - marius
显示剩余3条评论

1

仅有一个例外情况,即无法处理关键字参数以实现您想要的功能。当然,这是针对Python 2.7而言。

在Python中生成此消息的代码如下:

PyErr_Format(PyExc_TypeError,
    "%.200s() takes %s %d "
    "argument%s (%d given)",
    PyString_AsString(co->co_name),
    defcount ? "at most" : "exactly",
    co->co_argcount,
    co->co_argcount == 1 ? "" : "s",
    argcount + kwcount);

这段代码来自于 http://hg.python.org/cpython/file/0e5df5b62488/Python/ceval.c 的3056-3063行。

可以看到,异常信息中并没有给出足够的参数缺失信息。在这个上下文中,co是被调用的PyCodeObject。唯一给出的是一个字符串(如果你愿意,你可以解析它),包括函数名、是否有可变参数、期望的参数数量以及实际给出的参数数量。正如已经指出的那样,这并不能提供足够的信息来确定哪些参数未被给定(对于关键字参数的情况)。

inspect或其他调试模块可能能够提供足够的信息,以确定调用了哪个函数以及如何调用它,从而可以确定哪些参数未被给定。

但是需要注意的是,几乎肯定无论你想出什么解决方案,都无法处理至少一些扩展模块方法(用C编写的方法),因为它们不会将参数信息作为对象的一部分提供。


对于最后一个段落,在3.x版本中还有一件事情变得更好了。使用Argument Clinic定义的函数暴露出inspect.Signature对象。在3.4b3+ 中,大多数内置函数和标准库都是这样。在3.4之后,我相信目标是使其更适用于第三方扩展模块,包括那些与早期版本(不过,这是否意味着3.1+ 或 2.6+我不知道)兼容的模块。 - abarnert

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