Being able to access items in collections using subscripting (i.e. with square brackets, like
myCollection) 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
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.
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
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.
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).
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.
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.
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.
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 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;
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
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
) and use keyword arguments when needed, you can do something like
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.
Oh, and don’t forget about the assignment variations of each,
set(). Their signatures work exactly the same as the lookup versions except they have one additional parameter for the value being assigned.
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.