Misleading(?) TypeError when passing keyword arguments to function defined with positional arguments
In cPython 2.4:
def f(a,b,c,d):
pass
>>> f(b=1,c=1,d=1)
TypeError: f() takes exactly 4 non-keyword arguments (0 given)
but:
>>> f(a=1,b=1,c=1)
TypeError: f() takes exactly 4 non-keyword arguments (3 given)
Clearly, I don't really really understand Python's function-argument processing mechanism. Anyone care to share some light on this? I see what's happening (something like filling argum开发者_JS百科ent slots, then giving up), but I think this would foul up a newbie.
(also, if people have better question keywords -- something like "guts" -- please retag)
When you say
def f(a,b,c,d):
you are telling python that f
takes 4 positional arguments. Every time you call f
it must be given exactly 4 arguments, and the first value will be assigned to a
, the second to b
, etc.
You are allowed to call f
with something like
f(1,2,3,4)
or f(a=1,b=2,c=3,d=4)
, or even f(c=3,b=2,a=1,d=4)
but in all cases, exactly 4 arguments must be supplied.
f(b=1,c=1,d=1)
returns an error because no value has been supplied for a
. (0 given)
f(a=1,b=1,c=1)
returns an error because no value has been supplied for d
. (3 given)
The number of args given indicates how far python got before realizing there is an error.
By the way, if you say
def f(a=1,b=2,c=3,d=4):
then your are telling python that f
takes 4 optional arguments. If a certain arg is not given, then its default value is automatically supplied for you. Then you could get away with calling
f(a=1,b=1,c=1)
or f(b=1,c=1,d=1)
It is theoretically possible to wrap the resulting TypeError with something more clear and informative. However, there are many little details some of which I don't know how to solve.
NOTE: the code below is a barely-working example, not a complete solution.
try:
fn(**data)
except TypeError as e:
## More-sane-than-default processing of a case `parameter ... was not specified`
## XXX: catch only top-level exceptions somehow?
## * through traceback?
if fn.func_code.co_flags & 0x04: ## XXX: check
# it accepts `*ar`, so not the case
raise
f_vars = fn.func_code.co_varnames
f_defvars_count = len(fn.func_defaults)
## XXX: is there a better way?
## * it catches `self` in a bound method as required. (also, classmethods?)
## * `inspect.getargspec`? Imprecise, too (for positional args)
## * also catches `**kwargs`.
f_posvars = f_vars[:-f_defvars_count]
extra_args = list(set(data.keys()) - set(f_vars))
missing_args = list(set(f_posvars) - set(data.keys()))
if missing_args: # is the case, raise it verbosely.
msg = "Required argument(s) not specified: %s" % (
', '.join(missing_args),)
if extra_args:
msg += "; additionally, there are extraneous arguments: %s" % (
', '.join(extra_args))
raise TypeError(msg, e)
#_log.error(msg)
#raise
raise
精彩评论