18

How can the range function take either: a single argument, range(stop), or range(start, stop), or range(start, stop, step). Does it use a variadic argument like *arg to gather the arguments, and then use a series of if statements to assign the correct values depending on the number of arguments supplied? In essence, does range() specify that if there is one argument, then it set as the stop argument, or if there are two then they are start, and stop, or if there are three then it sets those as stop, start, and step respectively? I'd like to know how one would do this if one were to write range in pure CPython.

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
MWGriffin
  • 337
  • 1
  • 3
  • 11

4 Answers4

8

Range takes, 1, 2, or 3 arguments. This could be implemented with def range(*args), and explicit code to raise an exception when it gets 0 or more than 3 arguments.

It couldn't be implemented with default arguments because you can't have a non-default after a default, e.g. def range(start=0, stop, step=1). This is essentially because python has to figure out what each call means, so if you were to call with two arguments, python would need some rule to figure out which default argument you were overriding. Instead of having such a rule, it's simply not allowed.

If you did want to use default arguments you could do something like: def range(start=0, stop=object(), step=1) and have an explicit check on the type of stop.

Marcin
  • 48,559
  • 18
  • 128
  • 201
  • 1
    Your latter definition gives a syntax error (3.3 here, but AFAIK it's invalid in 2.x too). –  Nov 13 '12 at 18:18
  • 1
    Default arguments can only be at the end, so you’d have to switch the parameters around if you wanted to implement this in pure Python without varargs. – poke Nov 13 '12 at 18:25
  • 1
    I wrote my own `range`-like function once and did so by checking how many `*args` it was passed and from that determining what the various parameters meant (and assigning proper defaults for the others). Kind of a pain...but definitely doable. – martineau Nov 13 '12 at 19:16
  • @Marcin Thanks for your reply! It confirms my thoughts. I believe it would throw an error if there were more than 3, not 4 however. – MWGriffin Nov 13 '12 at 21:47
  • @MWGriffin Quite. Corrected. – Marcin Nov 13 '12 at 21:48
  • @martineau Those are the lines that I was thinking along. – MWGriffin Nov 13 '12 at 21:48
  • @Marcin: I'm curious about how you envision the `stop=object()` idea working because it seems to me like one would need something like `NotPresent=object()` then a `stop=NotPresent` in the function definition in order to check the value of the keyword reliably. – martineau Nov 13 '12 at 22:08
  • @martineau In this case, all you need to do is either check that the argument is an `int`, or catch the resulting exception when you try to perform arithmetic on an `object`. – Marcin Nov 13 '12 at 22:17
6

The beauty of open-source software is that you can just look it up in the source:

(TL;DR: yes, it uses varargs)

if (PyTuple_Size(args) <= 1) {
    if (!PyArg_UnpackTuple(args, "range", 1, 1, &stop))
        return NULL;
    stop = PyNumber_Index(stop);
    if (!stop)
        return NULL;
    start = PyLong_FromLong(0);
    if (!start) {
        Py_DECREF(stop);
        return NULL;
    }
    step = PyLong_FromLong(1);
    if (!step) {
        Py_DECREF(stop);
        Py_DECREF(start);
        return NULL;
    }
}
else {
    if (!PyArg_UnpackTuple(args, "range", 2, 3,
                           &start, &stop, &step))
        return NULL;

    /* Convert borrowed refs to owned refs */
    start = PyNumber_Index(start);
    if (!start)
        return NULL;
    stop = PyNumber_Index(stop);
    if (!stop) {
        Py_DECREF(start);
        return NULL;
    }
    step = validate_step(step);    /* Caution, this can clear exceptions */
    if (!step) {
        Py_DECREF(start);
        Py_DECREF(stop);
        return NULL;
    }
}
lqc
  • 7,434
  • 1
  • 25
  • 25
5

lqc's answer demonstrates how range is implemented in C. You may still be curious about how range would be implemented if Python's built in functions were written in Python. We can read PyPy's source code to find out. From pypy/module/__builtin__/functional.py:

def range_int(space, w_x, w_y=NoneNotWrapped, w_step=1):
    """Return a list of integers in arithmetic position from start (defaults
to zero) to stop - 1 by step (defaults to 1).  Use a negative step to
get a list in decending order."""

    if w_y is None:
        w_start = space.wrap(0)
        w_stop = w_x
    else:
        w_start = w_x
        w_stop = w_y

The first argument, space, appears as an argument in all the built-in functions I saw, so I'm guessing it's kind of like self in that the user does not directly supply it. Of the remaining three arguments, two of them have default values; so you can call range with one, two, or three arguments. How each argument is interpreted depends upon how many arguments were supplied.

Stevoisiak
  • 23,794
  • 27
  • 122
  • 225
Kevin
  • 74,910
  • 12
  • 133
  • 166
0

class xrangeIterator:

def __init__(self,xrange_obj):

    self._xrange_obj=xrange_obj


def __next__(self):

    self.copy_gen=self._xrange_obj._result

    for i in self.copy_gen:

        return i

    raise StopIteration

class xrange:

def __init__(self,*args):

    self._args=args

    self.start,self.step=0,1

    self._repr_string=None

    for i in self._args:

        if type(i) is not int:

            raise TypeError("Cannot interprate '{}' as integer!".format(type(i).__name__))


    if self._length(self._args)<1:

        raise TypeError( "xrange Must have at least one argument")


    elif self._length(self._args)==1:

        self.stop=self._args[0]

        self._repr_string="xrange(0,{})".format(self.stop)

        self._result=self._xrange(self.start-self.step,self.stop-self.step,self.step)

    elif self._length(self._args)==2:

        self.start=self._args[0]

        self.stop=self._args[1]

        self._result=self._xrange(self.start-self.step,self.stop-self.step,self.step)

    elif self._length(self._args)==3:

        self.step=self._args[2]

        self.start=self._args[0]

        self.stop=self._args[1]

        self._result=self._xrange(self.start-self.step,self.stop-self.step,self.step)


    else:

        raise TypeError("xrange expected at most three arguments,got {}".format(self._length(self._args)))


def __repr__(self):

    if self._length(self._args)==1:

        return self._repr_string

    return "xrange{}".format(self._args)

def __iter__(self):

    return xrangeIterator(self)


def _length(self,n):

    counter=0

    for i in n:

        counter+=1

    return counter


def _xrange(self,start,stop,step):

    if step==0:

            raise ValueError("Argument 3 should not be zero!")

    if start<stop and step<0:

            raise TypeError("argument 3 should not be {}".format(step))

    if start<stop:

        while start<stop:

            start+=step

            yield start

    else:

        while start>stop and step<0:

            start+=step

            yield start