Forced naming of parameters in Python

2020-01-27 01:13发布

问题:

In Python you may have a function definition:

def info(object, spacing=10, collapse=1)

which could be called in any of the following ways:

info(odbchelper)                    
info(odbchelper, 12)                
info(odbchelper, collapse=0)        
info(spacing=15, object=odbchelper)

thanks to Python's allowing of any-order arguments, so long as they're named.

The problem we're having is as some of our larger functions grow, people might be adding parameters between spacing and collapse, meaning that the wrong values may be going to parameters that aren't named. In addition sometimes it's not always clear as to what needs to go in. We're after a way to force people to name certain parameters - not just a coding standard, but ideally a flag or pydev plugin?

so that in the above 4 examples, only the last would pass the check as all the parameters are named.

Odds are we'll only turn it on for certain functions, but any suggestions as to how to implement this - or if it's even possible would be appreciated.

回答1:

In Python 3 - Yes, you can specify * in the argument list.

From docs:

Parameters after “*” or “*identifier” are keyword-only parameters and may only be passed used keyword arguments.

>>> def foo(pos, *, forcenamed):
...   print(pos, forcenamed)
... 
>>> foo(pos=10, forcenamed=20)
10 20
>>> foo(10, forcenamed=20)
10 20
>>> foo(10, 20)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: foo() takes exactly 1 positional argument (2 given)

This can also be combined with **kwargs:

def foo(pos, *, forcenamed, **kwargs):


回答2:

You can force people to use keyword arguments in Python3 by defining a function in the following way.

def foo(*, arg0="default0", arg1="default1", arg2="default2"):
    pass

By making the first argument a positional argument with no name you force everyone who calls the function to use the keyword arguments which is what I think you were asking about. In Python2 the only way to do this is to define a function like this

def foo(**kwargs):
    pass

That'll force the caller to use kwargs but this isn't that great of a solution as you'd then have to put a check to only accept the argument that you need.



回答3:

True, most programming languages make parameter order part of the function call contract, but this doesn't need to be so. Why would it? My understanding of the question is, then, if Python is any different to other programming languages in this respect. In addition to other good answers for Python 2, please consider the following:

__named_only_start = object()

def info(param1,param2,param3,_p=__named_only_start,spacing=10,collapse=1):
    if _p is not __named_only_start:
        raise TypeError("info() takes at most 3 positional arguments")
    return str(param1+param2+param3) +"-"+ str(spacing) +"-"+ str(collapse)

The only way a caller would be able to provide arguments spacing and collapse positionally (without an exception) would be:

info(arg1, arg2, arg3, module.__named_only_start, 11, 2)

The convention of not using private elements belonging to other modules already is very basic in Python. As with Python itself, this convention for parameters would only be semi-enforced.

Otherwise, calls would need to be of the form:

info(arg1, arg2, arg3, spacing=11, collapse=2)

A call

info(arg1, arg2, arg3, 11, 2)

would assign value 11 to parameter _p and an exception risen by the function's first instruction.

Characteristics:

  • Parameters before _p=__named_only_start are admitted positionally (or by name).
  • Parameters after _p=__named_only_start must be provided by name only (unless knowledge about the special sentinel object __named_only_start is obtained and used).

Pros:

  • Parameters are explicit in number and meaning (the later if good names are also chosen, of course).
  • If the sentinel is specified as first parameter, then all arguments need to be specified by name.
  • When calling the function, it's possible to switch to positional mode by using the sentinel object __named_only_start in the corresponding position.
  • A better performance than other alternatives can be anticipated.

Cons:

  • Checking occurs during run-time, not compile-time.
  • Use of an extra parameter (though not argument) and an additional check. Small performance degradation respect to regular functions.
  • Functionality is a hack without direct support by the language (see note below).
  • When calling the function, it's possible to switch to positional mode by using the sentinel object __named_only_start in the right position. Yes, this can also be seen as a pro.

Please do keep in mind that this answer is only valid for Python 2. Python 3 implements the similar, but very elegant, language-supported mechanism described in other answers.

I've found that when I open my mind and think about it, no question or other's decision seems stupid, dumb, or just silly. Quite on the contrary: I typically learn a lot.



回答4:

You can do that in a way that works in both Python 2 and Python 3, by making a "bogus" first keyword argument with a default value that will not occur "naturally". That keyword argument can be preceded by one or more arguments without value:

_dummy = object()

def info(object, _kw=_dummy, spacing=10, collapse=1):
    if _kw is not _dummy:
        raise TypeError("info() takes 1 positional argument but at least 2 were given")

This will allow:

info(odbchelper)        
info(odbchelper, collapse=0)        
info(spacing=15, object=odbchelper)

but not:

info(odbchelper, 12)                

If you change the function to:

def info(_kw=_dummy, spacing=10, collapse=1):

then all arguments must have keywords and info(odbchelper) will no longer work.

This will allow you to position additional keyword arguments any place after _kw, without forcing you to put them after the last entry. This often makes sense, e.g. grouping thing logically or arranging keywords alphabetically can help with maintenance and development.

So there is no need to revert to using def(**kwargs) and losing the signature information in your smart editor. Your social contract is to provide certain information, by forcing (some of them) to require keywords, the order these are presented in, has become irrelevant.



回答5:

Update:

I realized that using **kwargs would not solve the problem. If your programmers change function arguments as they wish, one could, for example, change the function to this:

def info(foo, **kwargs):

and the old code would break again (because now every function call has to include the first argument).

It really comes down to what Bryan says.


(...) people might be adding parameters between spacing and collapse (...)

In general, when changing functions, new arguments should always go to the end. Otherwise it breaks the code. Should be obvious.
If someone changes the function so that the code breaks, this change has to be rejected.
(As Bryan says, it is like a contract)

(...) sometimes it's not always clear as to what needs to go in.

By looking at the signature of the function (i.e def info(object, spacing=10, collapse=1) ) one should immediately see that every argument that has not a default value, is mandatory.
What the argument is for, should go into the docstring.


Old answer (kept for completeness):

This is probably not a good solution:

You can define functions this way:

def info(**kwargs):
    ''' Some docstring here describing possible and mandatory arguments. '''
    spacing = kwargs.get('spacing', 15)
    obj = kwargs.get('object', None)
    if not obj:
       raise ValueError('object is needed')

kwargs is a dictionary that contains any keyword argument. You can check whether a mandatory argument is present and if not, raise an exception.

The downside is, that it might not be that obvious anymore, which arguments are possible, but with a proper docstring, it should be fine.



回答6:

The python3 keyword-only arguments (*) can be simulated in python2.x with **kwargs

Consider the following python3 code:

def f(pos_arg, *, no_default, has_default='default'):
    print(pos_arg, no_default, has_default)

and its behaviour:

>>> f(1, 2, 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() takes 1 positional argument but 3 were given
>>> f(1, no_default='hi')
1 hi default
>>> f(1, no_default='hi', has_default='hello')
1 hi hello
>>> f(1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() missing 1 required keyword-only argument: 'no_default'
>>> f(1, no_default=1, wat='wat')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() got an unexpected keyword argument 'wat'

This can be simulated using the following, note I've taken the liberty of switching TypeError to KeyError in the "required named argument" case, it wouldn't be too much work to make that the same exception type as well

def f(pos_arg, **kwargs):
    no_default = kwargs.pop('no_default')
    has_default = kwargs.pop('has_default', 'default')
    if kwargs:
        raise TypeError('unexpected keyword argument(s) {}'.format(', '.join(sorted(kwargs))))

    print(pos_arg, no_default, has_default)

And behaviour:

>>> f(1, 2, 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() takes exactly 1 argument (3 given)
>>> f(1, no_default='hi')
(1, 'hi', 'default')
>>> f(1, no_default='hi', has_default='hello')
(1, 'hi', 'hello')
>>> f(1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in f
KeyError: 'no_default'
>>> f(1, no_default=1, wat='wat')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in f
TypeError: unexpected keyword argument(s) wat

The recipe works equally as well in python3.x, but should be avoided if you are python3.x only



回答7:

You could declare your functions as receiving **args only. That would mandate keyword arguments but you'd have some extra work to make sure only valid names are passed in.

def foo(**args):
   print args

foo(1,2) # Raises TypeError: foo() takes exactly 0 arguments (2 given)
foo(hello = 1, goodbye = 2) # Works fine.


回答8:

def cheeseshop(kind, *arguments, **keywords):

in python if use *args that means you can pass n-number of arguments for this parameter - which will be come a list inside function to access

and if use **kw that means its keyword arguments, that can be access as dict - you can pass n-number of kw args, and if you want to restrict that user must enter the sequence and arguments in order then don't use * and ** - (its pythonic way to provide generic solutions for big architectures...)

if you want to restrict your function with default values then you can check inside it

def info(object, spacing, collapse)
  spacing = spacing or 10
  collapse = collapse or 1


回答9:

As other answers say, changing function signatures is a bad idea. Either add new parameters to the end, or fix every caller if arguments are inserted.

If you still want to do it, use a function decorator and the inspect.getargspec function. It would be used something like this:

@require_named_args
def info(object, spacing=10, collapse=1):
    ....

Implementation of require_named_args is left as an exercise for the reader.

I would not bother. It will be slow every time the function is called, and you will get better results from writing code more carefully.



回答10:

You could use the ** operator:

def info(**kwargs):

this way people are forced to use named parameters.



回答11:

I don't get why a programmer will add a parameter in between two others in the first place.

If you want the function parameters to be used with names (e.g. info(spacing=15, object=odbchelper) ) then it shouldn't matter what order they are defined in, so you might as well put the new parameters at the end.

If you do want the order to matter then can't expect anything to work if you change it!