How to implement python method with signature like ([start ,] stop [, step]), i.e. default keyword argument on the left
Asked Answered
O

5

6

Since in python 3.X the build-id range() function returns no longer a list but an iterable, some old code fails as I use range()to conveniently generate lists I need.

So I try to implement my own lrange function like this:

def lrange(start = 0, stop, step = 1):
    ret = []
    while start < stop:
        ret.append(start)
        start += step
    return ret

giving me a "non-default argument follows default argument" interpreter error.

If I look at Python's range() it seems to be possible.

I posted this question mainly because I was wondering if/how one can implement a function with such a signature on his own

Onaonager answered 26/12, 2011 at 15:44 Comment(0)
B
11

Quick Answer

This question popped up when I first started learning Python, and I think it worthwhile to document the method here. Only one check is used to simulate original behavior.

def list_range(start, stop=None, step=1):
    if stop is None:
         start, stop = 0, start
    return list(range(start, stop, step))

I think this solution is a bit more elegant than using all keyword arguments or *args.

Explanation

Use a Sentinel

The key to getting this right is to use a sentinel object to determine if you get a second argument, and if not, to provide the default to the first argument while moving the first argument to the second.

None, being Python's null value, is a good best-practice sentinel, and the idiomatic way to check for it is with the keyword is, since it is a singleton.

Example with a proper docstring, declaring the signature/API

def list_range(start, stop=None, step=1):
    '''
    list_range(stop) 
    list_range(start, stop, step)

    return list of integers from start (default 0) to stop,
    incrementing by step (default 1).

    '''
    if stop is None:
         start, stop = 0, start
    return list(range(start, stop, step))

Demonstration

>>> list_range(10)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> list_range(5, 10)
[5, 6, 7, 8, 9]
>>> list_range(2, 10, 2)
[2, 4, 6, 8]

And it raises an error if no arguments are given, unlike the all-keyword solution here.

Caveat

By the way, I hope this is only considered from a theoretical perspective by the reader, I don't believe this function is worth the maintenance, unless used in a central canonical location to make code cross-compatible between Python 2 and 3. In Python, it's quite simple to materialize a range into a list with the built-in functions:

Python 3.3.1 (default, Sep 25 2013, 19:29:01) 
[GCC 4.7.3] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> list(range(10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Batrachian answered 6/1, 2014 at 2:15 Comment(1)
+1 Only one check is enough to simulate the original behaviour :)Autobiographical
C
5

I've never actually given this much thought, but first of all, to solve your problem you should just be able to wrap range(...) with list(range(...)) in you're code.

Using keyword arguments, you could implement a signature like that since you are not required to specify the actual key when calling

def f(x=None, y=None, z=None):
    print x, y, z

f(1, 2, 3)
#output: 1 2 3

Then, you could inspect the values to determine how you should handle them. So to emulate range

def f(x=None, y=None, z=None):
    if z is not None: # then all values were assigned
        return range(x, y, z)
    elif y is not None: # then a start stop was set
        return range(x, y):
    else: # only one value was given
        return range(x)

The point here isn't to be a wrapper for range (as above, just use list) but rather to give some insight on if one was actually trying to emulate the builtin range signature for something custom.

Also keep in mind this isn't a complete solution, f(z=1) could cause problems with the above, so you want to provide sane defaults for each [kwarg] while checking for required kwarg

def f(x=0, y=None, z=1):
    if y is None:
        raise Exception()
    return range(x, y, z)

would be a little more insightful to a python method with a signature like ([start], stop, [step])

Counterbalance answered 26/12, 2011 at 15:50 Comment(2)
The last def requires a keyword arg for a single argument usage, the second will attempt range(None) if no args are used, raising an error that would be quite confusing to the user. I think this approach is superior: #8637630Batrachian
as noted in the my comments, the examples are for insight and favor consistent style over completeness. The answer you linked to addresses the question more directly anyway so +1 to yaCounterbalance
J
2

What about:

>>> def my_range(*args):
...     return list(range(*args))
... 
>>> my_range(0, 10, 2)
[0, 2, 4, 6, 8]
Jariah answered 26/12, 2011 at 15:50 Comment(1)
nice. This is a smart solution for my problem, and I'm going to use this. But I'm also interested in how to write a method with such a signatureOnaonager
A
1

You can try something like this:

def lrange(*args):
    # Default values
    start = 0
    step = 1

    # Assign variables based on args length
    if len(args) == 1:
        stop, = args
    elif len(args) == 2:
        start, stop = args
    elif len(args) == 3:
        start, stop, step = args
    else:
        raise TypeError('lrange expected at most 3 arguments, got {0}'
                        .format(len(args)))
     ...

If you try to use range in the interpreter, you'll see that it doesn't accept keyword arguments, so it's certainly playing some trick around variable number of arguments as in the example above.

Atwekk answered 26/12, 2011 at 15:52 Comment(4)
how it gets this signature then?Onaonager
range is probably not using keyword arguments to be more efficient, functioning as a builtin optimized to hell and back. I think for a general purpose function, keyword args would probably be alot more friendly, but this works just as well tooCounterbalance
The signature you're talking about is a description from the documentation. Looking at the code, is isn't even implemented in python, but in c with the following signature: static PyObject * builtin_range(PyObject *self, PyObject *args)Atwekk
actually i didn't ask the question :p just answered above about using kwargs, but in using cython i've noticed the impact of kwargs in situations so it makes sense that something like range would look like thisCounterbalance
F
1

You can use the *args and **kwargs features:

def myrange(*args, **kwargs):
  start = 0
  stop = None
  if (len(args) == 1):
    stop = args[0]
  if (len(args) >= 2 ):
    start = args[0]
    stop = args[1]
  start = kwargs.get("start", start)
  stop = kwargs.get("stop", stop)
  return list(range(start, stop))

You'd need to put in some more error checking, and support the step operator.

In this case, you're probably better off implementing it like this:

def myrange(*args):
  return list(range(*args))

Note that the builitn range funcion doesn't support kwargs.

Fremd answered 26/12, 2011 at 16:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.