Ranges and Slices in Julia and Python

Using ranges to get or set parts of an array

Ranges can look quite similar in both Python and Julia, however the apparent similarity can be deceptive as they work entirely different.

Here is an example of creating an array and accessing a subset of it in Julia. Notice that Julia has 1-based indices, meaning first element is at index 1.

julia> a = [2, 4, 6, 8]
julia> a[2:4]
3-element Array{Int64,1}:
4
6
8

The example in Python looks very similar:

python> a = [2, 4, 6, 8]
python> a[2:4]
[6, 8]

But here is where it starts getting different. In Julia : is an operator just like + or -. Operators in Julia are just syntax sugar for function calls. So 3 + 4 in Julia is just syntax sugar for +(3, 4), to avoid ambiguity one writes (+)(3, 4). Thus in Julia 2:4 is the same as writing (:)(2,4).

This has implications for how we use it. In Julia I can put a range into an object using the colon syntax:

julia> r = 2:4
2:4

julia> a[r]
3-element Array{Int64,1}:
4
6
8

julia> typeof(r)
UnitRange{Int64}

julia> r.start
2

julia> r.stop
4

If we check the step, you can see that does not exist for a UnitRange.

julia> r.step
ERROR: type UnitRange has no field step

However we can explicitly specify a step with the colon syntax.

julia> r = 1:2:length(a)
1:2:3
julia> typeof(r)
StepRange{Int64,Int64}
julia> r.step
2

This allows us to extract every other elements from the array a:

julia> a[r]
2-element Array{Int64,1}:
2
6

What helps understand how Julia actually operates is that so much of Julia’s functionality is written in Julia itself. To open a preconfigured editor at the file and line containing the code for the function called you can use the @edit macro. So e.g. if I want to know how range access in arrays are implemented I can write:

julia> @edit a[2:4]

This opens up the julia/base/array.jl file at line 736, showing me this code:

function getindex(A::Array, I::UnitRange{Int})
@_inline_meta
@boundscheck checkbounds(A, I)
lI = length(I)
X = similar(A, lI)
if lI > 0
unsafe_copyto!(X, 1, A, first(I), lI)
end
return X
end

If instead I try to use a single integer index:

julia> @edit a[2]

I will get this code:

@eval getindex(A::Array, i1::Int) = arrayref($(Expr(:boundscheck)), A, i1)

This is not that readable if you don’t have some deeper understanding of Julia. I could write a wrong but simplified version of this for easier understanding:

getindex(A::Array, i::Int) = arrayref(A, i)

arrayref is a builtin function implemented in C and not Julia code. This means that the [] index operator is not really implemented at C level. Every place [] is used, Julia will call getindex. As you've seen in the examples getindex is overloaded, so there will be different implementations called depending on what sort of index you are using.

Okay enough about Julia. Let us study the situation in Python land more. In Python : is not an operator producing range objects. This example proves that:

python> r = 2:4
File "<stdin>", line 1
r = 2:4
^
SyntaxError: invalid syntax

Instead you must use the range function. Its behavior is different between Python 2.x and Python 3.x. In 2.x it will create an array:

python> range(2, 6)
[2, 3, 4, 5]

Python has both arrays and lists, for the cases where Julia would simple use arrays. Most of the time when talking about arrays, I am implying lists in python, since they serve as similar usage to arrays in Julia.

However in Python 3.x we instead get a range object, which is more similar to Julia:

python> r = range(1, 3)
python> r.start
1
python> r.stop
3
python> r.step
1

Which you can see have similar properties. However Python does not differentiate between UnitRange and StepRange.

However if we try to use a range, we quickly discover that does not work:

a[r]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: list indices must be integers or slices, not range

Python distinguishes between ranges and slices. You can use a range in say a for-loop, however to access a range of array elements you need to use a slice object.

python> s = slice(1, 3)
python> s
slice(1, 3, None)
python> a[s]
[4, 6]

You can see that the properties of a slice can be inspected much the same way as a range in either Julia or Python:

python> s = slice(1, 4, 2)
python> s
slice(1, 4, 2)
python> s.start
1
python> s.stop
4
python> s.step
2

Another example of their differences is when creating lists.

python> list(range(5, 8))
[5, 6, 7]
python> list(slice(5, 8))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'slice' object is not iterable

You can see that ranges can be used but not slices. In Julia this would not make a difference. You could write:

julia> Array(5:8)
4-element Array{Int64,1}:
5
6
7
8

Or even use the ... for exploding arrays into arguments.

julia> [5:8...]
4-element Array{Int64,1}:
5
6
7
8

Relearning Python after having used Julia for many years, I this as just another case, where I think Python lacks coherency and clarity.

I think Julia presents a more elegant design which is more flexible. By letting the : construct range objects, you can merge several usage patterns into one.

The difficulty of accomplishing this in Python is most likely due to the fact that Python does not allow function overloading in the same way. Python is a single dispatch language, which means only one object can be used to pick the right function to execute at run time.

Julia in contrast supports multiple dispatch, meaning every argument to a function determines which particular implementation gets picked at runtime. This makes it possible to define range and index access for different types of objects easily.

Ironically the high performance of Julia makes it a more user-friendly language. Due to the high performance of Julia code, much more of the Julia functionality is implemented in Julia itself and don’t represent special cases as with Python. Thus using the @edit macro is a very effective way of inspecting how almost anything works in Julia. In Python I don't have access to any similar functionality, which makes me quickly check how some particular functionality works in Python.

Written by

Geek dad, living in Oslo, Norway with passion for UX, Julia programming, science, teaching, reading and writing.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store