Python Tips: range (Function or Class) in Details
Tags: python, tutorial, programming
Published: 2023-12-17 23:50:17

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
```

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
>>> 

```

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'>

```
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)
```

## 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']
>>> 
```
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
>>> 
```

# 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/shybclz57xvc333x4c34.png)

```
>>> 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
```

# 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;

```

## 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));
}
```

### 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);
```

- 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;
}
```

### 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;
}
```

## 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
```

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;
}

```

### 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
```

# 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