Reputation: 99
I'd like to declare a function with a non-default (mandatory) parameter preceded by a default parameter, just as in the built-in range function: range(start, end, step)
Is there anyway to do that? When I try to define a function that way I get a SyntaxError
as in def example(a = 0, b)
Upvotes: 1
Views: 970
Reputation: 565
In Python, positional arguments must come before keyword arguments – it's just a rule of the language. That said, there are a few ways (that I wouldn't necessarily recommend) that you could write a function to get around this.
This is essentially how the built-in range
function does what you're describing:
def range(*args):
# default values
start = 0
step = 1
n_args = len(args)
if not 0 < n_args < 4:
raise TypeError(f'range expected 1 arguments, got {n_args}')
elif n_args == 1:
stop = args[0]
elif n_args == 2:
start, stop = args
else:
start, stop, step = args
# etc...
It requires arguments be passed in a specific order, then determines what variables to assign values to based on the number of arguments passed.
Or, consider the following:
def example(a=10, b=None):
assert b is not None, "argument 'b' is required"
# etc...
This is an even uglier solution, because:
b
as a keyword argument, not a positional argumentb
is required – it looks like it's optional, but the function will fail if it's not explicitly passed.Unless there's a really compelling reason not to, it's best to stick to the usual format (positional arguments first, optionally followed by collecting leftover *args
, then keyword arguments, optionally followed by collecting leftover **kwargs
). It's how people expect to use a function and will make your code easier to both use and read. If there really is a compelling reason to deviate from that, mimic the setup of range
.
re: your question about "correcting" the function signature to show certain parameter names instead of *args
, the example above massively simplifies how the range
built-in works. In reality, it's implemented in C and its signature/docstring are set here. It's more analogous to a class that defines a __call__
method that gets run inside the constructor than a regular function.
Manually overriding an object's signature would be tricky (not to mention a whole lot of extra steps to avoid using the "normal" parameter order) -- you're much better off just putting the "correct" parameter names in the docstring.
There are some really, really hacky ways you could do it. E.g., you could wrap the function in a decorator that uses something like inspect.Signature
to modify the function object. Or maybe you could convert it to a class, have it inherit from some parent class, then use a combination of __init_subclass__
in the parent and __new__
in the child to intercept and modify the arguments passed to the constructor. But these solutions are so far off in left field that it's worth asking why there isn't a simpler way to solve this problem. In my opinion, it's because this simply isn't something you want to be doing in the first place.
Upvotes: 2
Reputation: 155507
You can't. The way range
's parameters are defined is a feature of range
being a built-in and using a different means of parsing arguments, roughly equivalent to receiving *args, **kwargs
and hand-parsing them (it rejects non-empty kwargs
manually, but still has to accept them due to the conventions of CPython's C API). If you want to do it yourself, you can, but Python won't help:
def myrange(*args):
if not args or len(args) > 3:
raise TypeError("Expected 1-3 args")
if len(args) == 1:
start, step = 0, 1
stop, = args
else:
start, stop, step = (args + (1,))[:3] # Stupid trick to reduce number of cases
# Actual work done here
Upvotes: 0
Reputation: 9766
You can't. Default arguments must be last in the argument list. If you want different behavior, then you'll have to do the validation inside the function.
def primes_in_interval(start=2, end=None):
assert(end is not None)
# Rest of code.
primes_in_interval(end=10)
You could do something like this to get the calling convention you're looking for.
def primes_in_interval(end_or_start, end=None):
if end is None:
start = 2
end = end_or_start
else:
start = end_or_start
end = end
# Rest of code.
primes_in_interval(10) # Start will be 2 and end 10
primes_in_interval(8, 16) # Start will be 8 and end 16
Upvotes: 0