Python

Subscript Operator Overloading: Python vs Kotlin

Being able to access items in collections using subscripting (i.e. with square brackets, like myCollection[2]) is a really big convenience for me. I hate typing method names for this functionality, especially the boring old get() method. Not only is get() boring, it’s incredibly nondescript. (On a tangent: In my opinion, it would be nice if the “default” name for a method like this was sub() or subscript(), but too few people even know the term “subscript”).

Both Python and Kotlin allow you to use operator overloading in order to get this functionality, and in this article, I’m going to do a medium dive into each one, comparing and contrasting their limitations and how they work.

Python

We’ll start with Python’s version of it. As with all operator overloading in Python, it used a dunder method. Specifically, it starts with def __getitem__(self, key): which is invoked with self[key]

Now, that sample invocation is extremely misleading; it makes it seem like you can only pass in one thing, and the natural inclination is that that thing has to be an integer index. Both of these implications are wrong. You can pass in a comma-separated list of many arguments of whatever type you want.

When you pass in multiple arguments, the key parameter is given a tuple of all of them, so you’ll have to split and parse that out yourself. 

I love the idea of multiple indices passed in when it comes to multi-dimensional containers. A 2D array of pixel data can be accessed with pixels[x, y] instead of pixels[x][y], as I’ve typically seen. It’s highly underutilized.

Slicing

Another interesting feature of subscripting in Python is “slicing”. For those who don’t know what that is, here’s a nice primer by GeeksforGeeks. When you include a slice argument in the brackets, it’s turned into a slice object, which is mostly just a tuple of the three arguments (using None for empty parts). 

But the slice object has one extra small feature as well: the indices() method. This is helpful for when any of the numbers in the slice are negative or missing; you pass in the length of the collection, and it will return a tuple of the positive indices (a.k.a. the “true” indices) those numbers mean. This only works if the slice contains integers, of course. For example, if the slice -10::3 was passed into __getitem__(), you’d get a slice(-10, None, 3) object. When you call indices(30) on it, it will return the tuple (20, 30, 3). With that, it becomes really easy to figure out which indices to use. Even easier when you realize you can pass them into range() with the spread operator and get the entire sequence of related indices. So if your class is largely a wrapper around another collection that uses indices, but not slices, a simple implementation is this:

def __getitem__(self, key):
    if isinstance(key, slice):
        indices = key.indices(len(self))
        return [self.wrapped[idx] for idx in range(*indices)]
    else:
        return self.wrapped[key]

Interesting aside: If, in the example above, I had passed in -40 instead of -10, the first number in the returned tuple would have been 0. If a negative number would put the index lower than 0 even after adding the length, it caps it at 0.

Typing

When it comes to restricting what can be passed in as an index key, you need to use type annotations. If you’re allowing a lot of different options, that Union can get quite long (or you can use @typing.overload). If you’re allowing slicing, but you want to use something other than integers (for example, if you have a list of words and you want ones from “a” through “d”), you can’t restrict it at compile time because the slice type doesn’t have typing set up for its values, let alone generics. You’ll either have to use comma-separated values for slicing or create your own slice type to wrap around the numbers.

Kotlin

For Kotlin, the method (or extension) signature is far more variable. You’ve got the usual operator fun keyword combo then get(...). From there, the parameter list and return type can be just about whatever you want. And you can make as many overloads for it as you wish. 

Slicing

Really, the only sad part is the lack of real slice support. Again, like in Python for special cases, you could always use a triple of parameters or a slice type that gets passed in, but there is something else that you can use that’s similar, though still a bit more verbose. First, the method should accept an IntProgression (or an IntRange if you don’t care about the third part of a slice). Then you can pass in something like 1..100, 1 until 100, or 100 downTo 1, and add step 2 after that if you’d like to add in the third number. When you use .., it makes an inclusive range (up to and including the last number), unlike anything in Python; until and downTo create half-closed ranges (up to but not including the last number), like what you’re usually used to.

Downsides of this include the fact that you can’t leave any of the numbers blank, and it’s limited to integers and long integers (there’s LongProgression and LongRange, too). Both can be dealt with by using a custom slice type. If you make the constructor parameter have default values (will have to be null) and use keyword arguments when needed, you can do something like slice(1, 2), slice(1, step=2), slice(end=10), etc. 

Overloading

The biggest benefit of Kotlin’s operator is true overloads. It potentially reduces the amount of code reuse within the multiple implementations of the methods, but each one reads clearly as doing the one and only thing they do. With Python, you have to make the one method cover every possible implementation unless you use multiple dispatch. But multiple dispatch is doing the type-checking at runtime when it could be at compile time, in Kotlin’s case.

Assignment Subscripting

Oh, and don’t forget about the assignment variations of each, __setitem__() and set(). Their signatures work exactly the same as the lookup versions except they have one additional parameter for the value being assigned.

Summary

I actually went into this article thinking that Kotlin’s get() operator didn’t have multiple-parameter support and forgetting about the nice range feature. But I’m glad to know that both pretty much support the same feature set, even if slicing is a bit more clumsy in Kotlin.

For the most part, the differences are what you’d expect from a dynamic language comparing to a static language.

I’m surprised at how difficult it is to find good documentation on what is possible with these methods, especially Python’s. The Python docs are incredibly low on details and don’t even make it clear that you can pass multiple arguments in. Kotlin’s docs don’t explain how to do anything, but their examples show that you can choose however many parameters you want.

Published on Java Code Geeks with permission by Jacob Zimmerman, partner at our JCG program. See the original article here: Subscript Operator Overloading: Python vs Kotlin

Opinions expressed by Java Code Geeks contributors are their own.

Jacob Zimmerman

Jacob is a certified Java programmer (level 1) and Python enthusiast. He loves to solve large problems with programming and considers himself pretty good at design.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

2 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
ruurd
ruurd
2 years ago

I actually read this article until I encountered this:

is a really big convenience for me

Why would I listen to someone that writes code he finds convenient? Maybe you should start writing code for the poor sod that has to maintain your crap in a year…

Jacob Zimmerman
2 years ago
Reply to  ruurd

Are you being serious? You claim to have “actually read” only 2 sentences? Nice.

In rebuttal:

  1. I don’t know anyone who doesn’t prefer to use subscripting on their collections instead of .get(1), which is nondescript, or a longer, more tedious name.
  2. I AM that “poor sod”. I haven’t gotten to either of these languages professionally yet, so it’s only me, and I’m extremely glad I used subscripting.
Back to top button