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.
>>> 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
Top comments (0)