DEV Community

Fei
Fei

Posted on • Edited on

Python Tips: range (Function or Class) in Details

In Python, when we want to generate sequences of numbers and write a for loop to check the numbers, we may use range(), for example:

>>> for i in range(5):
...     print(i)
... 
Output:
0
1
2
3
4
Enter fullscreen mode Exit fullscreen mode

However, do you really know the function range()? What does range() function return? Do you know the class range?

Introduction

The range function is within Python Built-in Types.
The range type represents an immutable sequence of numbers and is commonly used for looping a specific number of times in for loops[1]. It includes three arguments:

range(start, stop[, step])
start
The first value you want to generate (or 0 if the parameter was not supplied)
stop
The last value value you want to generate, it not within the result
step
The value of the step parameter (or 1 if the parameter was not supplied), can be positive(Increase) or negative(Decrease)

Let’s show some examples below:

>>> for i in range(5):
...     print(i)
... 
0
1
2
3
4
>>> 
>>> for i in range(0,5):
...     print(i)
... 
0
1
2
3
4
>>> 
>>> for i in range(0, 5, 2):
...     print(i)
... 
0
2
4
>>> for i in range(5, 0, -1):
...     print(i)
... 
5
4
3
2
1
>>> 

Enter fullscreen mode Exit fullscreen mode

Let's get into more details about range.

What type of return value from range()?

We can use type to check the return value from range, it shows <class ‘range’>

>>> x = range(5)
>>> type(x)
<class 'range'>

Enter fullscreen mode Exit fullscreen mode

In fact, class range in python is a class where derived from class xrange(), function range is more efficient than class range()because it is a built-in function and does not require creating a new object instance[6].

Using help range We can see the source code of range class

>>> help(range)
class range(object)
 |  range(stop) -> range object
 |  range(start, stop[, step]) -> range object
 |  
 |  Return an object that produces a sequence of integers from start (inclusive)
 |  to stop (exclusive) by step.  range(i, j) produces i, i+1, i+2, ..., j-1.
 |  start defaults to 0, and stop is omitted!  range(4) produces 0, 1, 2, 3.
 |  These are exactly the valid indices for a list of 4 elements.
 |  When step is given, it specifies the increment (or decrement).
 |  
 |  Methods defined here:
 |  
 |  __bool__(self, /)
 |      self != 0
 |  
 |  __contains__(self, key, /)
 |      Return key in self.
 |  
 |  __eq__(self, value, /)
 |      Return self==value.
 |  
 |  __ge__(self, value, /)
 |      Return self>=value.
 |  
 |  __getattribute__(self, name, /)
 |      Return getattr(self, name).
 |  
 |  __getitem__(self, key, /)
 |      Return self[key].
 |  
 |  __gt__(self, value, /)
 |      Return self>value.
 |  
 |  __hash__(self, /)
 |      Return hash(self).
 |  
 |  __iter__(self, /)
 |      Implement iter(self).
 |  
 |  __le__(self, value, /)
 |      Return self<=value.
 |  
 |  __len__(self, /)
 |      Return len(self).
 |  
 |  __lt__(self, value, /)
 |      Return self<value.
 |  
 |  __ne__(self, value, /)
 |      Return self!=value.
 |  
 |  __reduce__(...)
 |      Helper for pickle.
 |  
 |  __repr__(self, /)
 |      Return repr(self).
 |  
 |  __reversed__(...)
 |      Return a reverse iterator.
 |  
 |  count(...)
 |      rangeobject.count(value) -> integer -- return number of occurrences of value
 |  
 |  index(...)
 |      rangeobject.index(value) -> integer -- return index of value.
 |      Raise ValueError if the value is not present.
 |  
 |  ----------------------------------------------------------------------
 |  Static methods defined here:
 |  
 |  __new__(*args, **kwargs) from builtins.type
 |      Create and return a new object.  See help(type) for accurate signature.
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |  
 |  start
 |  
 |  step
 |  
 |  stop
(END)
Enter fullscreen mode Exit fullscreen mode

We can use dir() to check the attributes:

>>> dir(range)
['__bool__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'count', 'index', 'start', 'step', 'stop']
>>> 
Enter fullscreen mode Exit fullscreen mode

Here, we can see it includes __iter__, which means is an instance of iterable[2].

Iterable is an object which can be looped over or iterated over with the help of a for loop. Objects like lists, tuples, sets, dictionaries, strings, etc. are called iterables[3].

Here, range is iterable, but it is not an Iterator, we can check it below:

# code
from collections.abc import Iterable, Iterator
x = range(5)

if isinstance(x, Iterable):
  print(f"{x} is iterable")
else:
  print(f"{x} is not iterable")

if isinstance(x, Iterator):
  print(f"{x} is Iterator")
else:
  print(f"{x} is not Iterator")

... 
... 
range(0, 5) is iterable
range(0, 5) is not Iterator
>>> 
Enter fullscreen mode Exit fullscreen mode

The definition of range in Python doc

Sequence Types

Rather than being a function, range is actually an immutable sequence type, as documented in Ranges and Sequence Types — list, tuple, range [4].

class range(stop)
class range(start, stop, step=1)
Rather than being a function, range is actually an immutable sequence type, as documented in Ranges and Sequence Types — list, tuple, range.

Arguments

range(start, stop[, step])
The arguments to the range constructor must be integers (either built-in int or any object that implements the index() special method) [5].

Operations

Common sequence operations(totally 12 items)

Common sequence operations can be found here:

https://docs.python.org/3/library/stdtypes.html

Range objects implement the collections.abc.Sequence ABC, and provide features such as containment tests, element index lookup, slicing and support for negative indices (see Sequence Types — list, tuple, range)

The collections.abc.Sequence ABC is provided to make it easier to correctly implement these operations on custom sequence types.

Image description

>>> r = range(0, 20, 2)
>>> r
range(0, 20, 2)
>>> 
>>> 11 in r
False
>>> 
>>> 10 in r
True
>>> 
>>> r.index(10)
5
>>> 
>>> r[5]
10
>>> 
>>> r[:5]
range(0, 10, 2)
>>> 
>>> r[-1]
18
Enter fullscreen mode Exit fullscreen mode

Source code of range in C

We can check the range code in C from c code below:
https://github.com/python/cpython/blob/main/Objects/rangeobject.c

First, we can know the rangeobject.
We have the start, stop, step pointers, also we have length, which can be used to store the length of the range elements. PyObject_HEAD defines the initial segment of every PyObject.

typedef struct {
    PyObject_HEAD
    PyObject *start;
    PyObject *stop;
    PyObject *step;
    PyObject *length;
} rangeobject;

Enter fullscreen mode Exit fullscreen mode

Workflow when create a range object

1) When reading the code, we can follow the 3 main functions:

range_new->range_from_array->make_range_object

Range_new

It mainly call the range_from_array function.

static PyObject *
range_new(PyTypeObject *type, PyObject *args, PyObject *kw)
{
    if (!_PyArg_NoKeywords("range", kw))
        return NULL;

    return range_from_array(type, _PyTuple_ITEMS(args), PyTuple_GET_SIZE(args));
}
Enter fullscreen mode Exit fullscreen mode

range_from_array

Check the arguments(Py_ssize_t num_args), including start, stop, step,

  • If invalid arguments, return null, error
  • If start is none, use 0,
  • If step is none, use default value 1, >Here, we can see it will check step argument by step = validate_step(step); While, in validate_step, it will return 1 if step is null.
    /* No step specified, use a step of 1. */
    if (!step)
        return PyLong_FromLong(1);
Enter fullscreen mode Exit fullscreen mode
  • Finally make a object make_range_object
static PyObject *
range_from_array(PyTypeObject *type, PyObject *const *args, Py_ssize_t num_args)
{
    rangeobject *obj;
    PyObject *start = NULL, *stop = NULL, *step = NULL;

    switch (num_args) {
        case 3:
            step = args[2];
            /* fallthrough */
        case 2:
            /* Convert borrowed refs to owned refs */
            start = PyNumber_Index(args[0]);
            if (!start) {
                return NULL;
            }
            stop = PyNumber_Index(args[1]);
            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;
            }
            break;
        case 1:
            stop = PyNumber_Index(args[0]);
            if (!stop) {
                return NULL;
            }
            start = _PyLong_GetZero();
            step = _PyLong_GetOne();
            break;
        case 0:
            PyErr_SetString(PyExc_TypeError,
                            "range expected at least 1 argument, got 0");
            return NULL;
        default:
            PyErr_Format(PyExc_TypeError,
                         "range expected at most 3 arguments, got %zd",
                         num_args);
            return NULL;
    }
    obj = make_range_object(type, start, stop, step);
    if (obj != NULL) {
        return (PyObject *) obj;
    }

    /* Failed to create object, release attributes */
    Py_DECREF(start);
    Py_DECREF(stop);
    Py_DECREF(step);
    return NULL;
}
Enter fullscreen mode Exit fullscreen mode

make_range_object

Return the rangeobject by PyObject_New()

static rangeobject *
make_range_object(PyTypeObject *type, PyObject *start,
                  PyObject *stop, PyObject *step)
{
    rangeobject *obj = NULL;
    PyObject *length;
    length = compute_range_length(start, stop, step);
    if (length == NULL) {
        return NULL;
    }
    obj = PyObject_New(rangeobject, type);
    if (obj == NULL) {
        Py_DECREF(length);
        return NULL;
    }
    obj->start = start;
    obj->stop = stop;
    obj->step = step;
    obj->length = length;
    return obj;
}
Enter fullscreen mode Exit fullscreen mode

Other operation functions in C

Index

We can check the index from a range:

>>> x = range(5)
>>> list(x)
[0, 1, 2, 3, 4]
>>> x.index(3)
3
Enter fullscreen mode Exit fullscreen mode

In the C code, it first checks whether the target value in the range or not. If it contains the target value, it will call the PyNumber_Subtract() to get the distance from start to calculate the index.
PyObject *idx = PyNumber_Subtract(ob, r->start);

The whole function code is here:

static PyObject *
range_index(rangeobject *r, PyObject *ob)
{
    int contains;

    if (!PyLong_CheckExact(ob) && !PyBool_Check(ob)) {
        Py_ssize_t index;
        index = _PySequence_IterSearch((PyObject*)r, ob, PY_ITERSEARCH_INDEX);
        if (index == -1)
            return NULL;
        return PyLong_FromSsize_t(index);
    }

    contains = range_contains_long(r, ob);
    if (contains == -1)
        return NULL;

    if (contains) {
        PyObject *idx = PyNumber_Subtract(ob, r->start);
        if (idx == NULL) {
            return NULL;
        }

        if (r->step == _PyLong_GetOne()) {
            return idx;
        }

        /* idx = (ob - r.start) // r.step */
        PyObject *sidx = PyNumber_FloorDivide(idx, r->step);
        Py_DECREF(idx);
        return sidx;
    }

    /* object is not in the range */
    PyErr_Format(PyExc_ValueError, "%R is not in range", ob);
    return NULL;
}

Enter fullscreen mode Exit fullscreen mode

Compare two range objects

We can also compare two range objects, actually, it compare the length of range objects, cmp_result = PyObject_RichCompareBool(r0->length, r1->length, Py_EQ);

>>> x = range(5)
>>> y = range(5)
>>> x==y
True
>>> id(x)
4551323104
>>> id(y)
4551266592
Enter fullscreen mode Exit fullscreen mode

Conclusion:

We talked about how to use range and the source code behind it in Python and C! In summary:

  • range() is a built-in function, support 3 arguments: range(start, stop[, step])
  • In Python 3, range is a class, which means range() is the constructor function
  • It belongs to Sequence Types, it is iterable, not a iterator
  • It supports common operations including containment tests, element index lookup, slicing and support for negative indices, just as the operations in list
  • Range does not allow any of its parameters to be a float, should be integers

If you are interested, please check the Python doc and C code in Github!

Life is short, I use Python~

Reference:

[1] https://docs.python.org/3/library/stdtypes.html#range
[2] https://docs.python.org/3/glossary.html#term-iterable
[3] https://www.analyticsvidhya.com/blog/2021/07/everything-you-should-know-about-iterables-and-iterators-in-python-as-a-data-scientist/
[4] )https://docs.python.org/3/library/functions.html#func-range
[5] https://docs.python.org/3/library/stdtypes.html#typesseq-range
[6] https://www.copahost.com/blog/python-range/
[7] https://github.com/python/cpython/blob/main/Objects/rangeobject.c

Top comments (0)