Sequences and generators in Kotlin and Python, by example

Carmen Alvarez
19 min readJan 2, 2024

--

Why this article? 🤔

To be honest, I haven’t yet used Kotlin sequences or Python generators in very advanced ways in my personal or professional projects, beyond a few simple use cases: applying builtin sequence functions like filter and map to large collections in Kotlin, or some basic generator expressions in Python. I’ve seen Python generators presented at various talks, and recently discovered that similar APIs exist in Kotlin. I thought it would be interesting to play around with them, compare their APIs and subtle behavior differences between Kotlin and Python, and share some simple examples. Hopefully this article may be helpful for somebody coming from one of the languages and learning the other language, or for somebody who like me, hasn’t yet used generators much.

Concepts we’ll explore 💡

We’ll see:

  • How to adapt a Fibonacci calculation function into a Fibonacci iterator.
  • How to adapt our iterator into a Fibonacci sequence generator.
  • Different ways to create sequences/generators that transform items from one sequence/generator to another.
  • How to combine multiple sequences/generators for transforming and filtering.
  • How to yield one item at a time, or multiple items from another Iterable source.
  • Performance trade-offs with sequences/generators versus collections.

Context

Let’s say we want to print the first 10 numbers of the Fibonacci sequence.

An initial approach could be to implement a function fibonacci(index), which returns the fibonacci number at the given index of the Fibonacci sequence. We can then call this function in a loop:

Example 1 — function to return the nth Fibonaccci number

Kotlin:

fun fibonacci(index: Int): Int {
var value = 0
var nextValue = 1
repeat(index - 1) {
value = nextValue.also { nextValue += value }
}
return value
}

for (i in 0..10) {
println(fibonacci(i))
}

Python:

def fibonacci(index: int) -> int:
value, next_value = 0, 1

for _ in range(index):
(value, next_value) = (next_value, value + next_value)
return value

for i in range(0, 10):
print(fibonacci(i))

This works, and prints the following output for both programs:

0
1
1
2
3
5
8
13
21
34

A problem with this approach is that our fibonacci function, called 10 times, has to recalculate the first n values of the Fibonacci sequence, each time it’s called with an argument n. For 10 calls to the function, it does a total of 0 + 1 + 2 + 3 + … + 7 + 8 + 9 = 45 iterations. More specifically, it’s n(n+1)/2 iterations. This is a complexity of O(n²).

Sequences/generators are one way to resolve this problem and get us down to a linear complexity.

Introducing iterators

Before jumping into sequences and generators, let’s take a look at iterators, which are used by sequences and generators.

Example 2 — Using an iterator to return infinite Fibonacci numbers

Let’s adapt our fibonacci function so that instead of doing n calculations and returning a value at the end, it will return a value at each iteration, and will be iterable an infinite number of times.

Kotlin:

class FibonacciIterator : Iterator<Int> {
private var value = 0
private var nextValue = 1

override fun hasNext() = true

override fun next(): Int {
val result = value
value = nextValue.also { nextValue += value }
return result
}
}

val fibonacciIterator = FibonacciIterator()
println(fibonacciIterator.next()) // 0
println(fibonacciIterator.next()) // 1
println(fibonacciIterator.next()) // 1

Python:

class FibonacciIterator:
def __init__(self):
self.value, self.next_value = 0, 1

def __iter__(self):
return self

def __next__(self):
result = self.value
(self.value, self.next_value) = (self.next_value, self.value + self.next_value)
return result


fibonacci_iterator = FibonacciIterator()
print(next(fibonacci_iterator)) # 0
print(next(fibonacci_iterator)) # 1
print(next(fibonacci_iterator)) # 1

One difference between Iterators in Kotlin in Python is how to handle reaching the end: In Kotlin, the function hasNext() must be implemented for callers to know if there are more elements left. In Python, there’s no equivalent function: instead, when a caller calls __next__() and there are no more elements, the Iterator must raise a StopIteration.

We can call next() on our Fibonacci iterator an infinite number of times. In Kotlin, our hasNext() always returns true, and in Python, our __next__() never raises a StopIteration.

We probably won’t be wanting to call next() on our iterator directly though. We can use a for loop instead:

Kotlin:

for (fib in fibonacciIterator) {
println(fib)
}

Python:

for fib in fibonacci_iterator:
print(fib)

We no longer have a limit to the number of Fibonacci numbers to generate, so the program with the for loop continues indefinitely until it is manually interrupted. We’ll see in a bit how to generate a finite number of numbers.

For the first 10 values, the output is the same as our first example. However, now we only do 10 Fibonacci calculations for these values (as opposed to 45 for example 1). We now have a linear complexity of O(n).

Fibonacci sequence/generator

Using an iterator allows us to generate Fibonacci numbers with linear complexity, which is a first improvement. However, using sequences, which build on top of iterators, will allow further improvements:

  • A little bit less boilerplate for generating a sequence of Fibonacci numbers.
  • In Kotlin, access to a variety of extension functions defined on Sequence, but not on Iterator: map, filter, take… we’ll see examples of these later on.
  • Using the more appropriate tool. In Kotlin, sequences are better suited than iterators for an infinite generation of numbers. The iterators documentation says that iterators are for “traversing collection elements”. Our Fibonacci numbers are not in a Collection like a List or Set. The sequence documentation says “Unlike collections, sequences don’t contain elements, they produce them while iterating”. This is closer to what we need for Fibonacci numbers;

Let’s adapt our Fibonacci iterator into a sequence (Kotlin) and generator (Python).

Example 3 — Using a sequence or generator

In Kotlin, we have a few ways to implement a Fibonacci Sequence:

We can define a class which implements the Sequence interface. It has one function which returns an Iterator. We can return an instance of our existing iterator:

class FibonacciSequence : Sequence<Int> {
override fun iterator() = FibonacciIterator()
}

val fibonacciSequence = FibonacciSequence()

Note that we create a new instance of FibonacciIterator each time iterator() is called. This is one feature of Sequences: they can be iterated multiple times. This is appropriate for a Fibonacci sequence. In the case of a source which can’t be iterated multiple times (perhaps involving reading from the network?), we could assert that the the sequence be iterable only once, by using constrainOnce().

We can also take our existing iterator class, and convert it into a Sequence with asSequence():

val fibonacciSequence = FibonacciIterator().asSequence()

Finally, we can use the sequence builder function:

fun fibonacciSequence() = sequence {
var value = 0
var nextValue = 1

while (true) {
yield(value)
value = nextValue.also { nextValue += value }
}
}

We’ll focus on this last version, which doesn’t rely on the Iterator from example 2, and takes advantage of suspending functions.

Python:

from typing import Generator


def fibonacci_generator() -> Generator[int, None, None]:
value, next_value = 0, 1

while True:
yield value
(value, next_value) = (next_value, value + next_value)

In Python, we use a generator function, which is closest to Kotlin’s sequence builder approach.

Notice we have a “while true” infinite loop now. We no longer return a value: instead we yield each value.

Note that calling fibonacciSequence() or fibonacci_generator() doesn’t do any calculations: it just returns an instance of a Sequence (Kotlin) or a generator iterator (Python). Let’s get an instance of these generators and do a few iterations on them:

Kotlin:

val fibonacciIterator = fibonacciSequence().iterator()
println(fibonacciIterator.next()) // 0
println(fibonacciIterator.next()) // 1
println(fibonacciIterator.next()) // 1

Python:

fibonacci_gen = fibonacci_generator()
print(next(fibonacci_gen)) # 0
print(next(fibonacci_gen)) # 1
print(next(fibonacci_gen)) # 1

In Kotlin, if we want to call next() directly, we have to convert the Sequence back to an Iterator. In Python the generator function returns a generator iterator, on which we can call next() directly.

We probably won’t be wanting to call next() directly though. We can use a for loop instead. Now we can use the Kotlin sequence directly, without converting it to an iterator in our code:

Kotlin:

for (fib in fibonacciSequence()) {
println(fib)
}

Python:

for fib in fibonacci_generator():
print(fib)

We still don’t have a limit to the number of Fibonacci numbers to generate, so the program continues indefinitely until it is manually interrupted.

But before we look at how to get only the first 10 Fibonacci numbers, let’s try to understand what’s going on.

Let’s break it down.

In the first iteration in our for loop, the control enters the fibonacci function, enters its while loop, and executes the yield statement for the first value (0):

Kotlin:

fun fibonacciSequence() = sequence {
var value = 0
var nextValue = 1

while (true) {
yield(value)
// control supsended here

Python:

def fibonacci_generator() -> Generator[int, None, None]:
value, next_value = 0, 1

while True:
yield value
# control supsended here

This results in the yielded value 0 being “returned” to the for loop iterator. At this point, execution inside the fibonacci function pauses. Note that the yield function in Kotlin is a suspend function, whereas in Python yield is a keyword in the language.

The function’s state (containing the local variables value and next_value) is preserved. We’ll find them again in the next iteration.

Inside our for loop, we print the result, and the for loop resumes for the second iteration inside the fibonacci function:

Kotlin:

    while (true) {
// 3. yield next value
yield(value)
// 1. control resumes here.
// 2. calculate values for next iteration
value = nextValue.also { nextValue += value }
}

Python:

    while True:
# 3. yield next value
yield value
# 1. control resumes here.
# 2. calculate values for next iteration
(value, next_value) = (next_value, value + next_value)
  1. Control is returned to the fibonacci function, just after the yield statement.
  2. We find the values of our local variables as they were left before the previous yield. We calculate the values for the next fibonacci number.
  3. We go back to the top of the while loop for another iteration and yield the new value (1).

Example 4 — Only do 10 iterations

To not iterate an infinite number of times, we use take in Kotlin and islice in Python to stop the iteration after 10 times.

Kotlin:

for (result in fibonacciSequence().take(10)) {
println(result)
}

Python:

from itertools import islice

for result in islice(fibonacci_generator(), 10):
print(result)

In Kotlin, take is a method defined on our Sequence instance. In Python, the generator iterator object doesn’t have a take method. Instead, we have to use islice from the itertools module, which creates a new iterator wrapped around our generator iterator. The new iterator stops after 10 iterations.

Combining sequences/generators

Why bother with sequences or generators in the first place? After all, our current program only needs to print the numbers of Fibonacci sequence: we could just go back to our first example with the fibonacci function which takes an index argument, and we could print the values while we calculate them:

fun fibonacci(index: Int) {
var value = 0
var nextValue = 1
repeat(index - 1) {
println(value)
value = nextValue.also { nextValue += value }
}
}

fibonacci(10)

That would work in this example, but we’d soon hit limitations if we wanted to do other things with the Fibonacci numbers, from multiple places in a project. Maybe in one place we’d want the first 10 squares of Fibonacci numbers, and in another part of the project we’d want the first 10 cubes instead (maybe not the best illustration of a real-life application 😅). Using a sequence/generator allows us to separate the calculation of the Fibonacci sequence items from their usage in the project, while keeping the linear complexity.

Example 5: square all items in a sequence/generator

Let’s look at an example of a sequence/generator that squares all the items in another sequence/generator:

Kotlin:

fun Sequence<Int>.square() =
sequence {
for (item in iterator()) {
yield(item * item)
}
}

Python:

def square(input_iterable: Iterable[int]) -> Generator[int, None, None]:
for item in input_iterable:
yield item * item

In Kotlin, we define an extension function on Sequence. It returns a new Sequence using the sequence builder function. It yields the square of all items in its initial iterator. In Python, we define a new generator function that takes the input Iterable as an argument. Like Kotlin, it yields the square of each item in this initial Iterable.

Now let’s see how to combine the square sequence/generator with the fibonacci one, to get a sequence/generator of the squares of all numbers in the Fibonacci sequence:

Kotlin:

for (item in fibonacciSequence().square().take(10)) {
println(item)
}

Python:

for item in islice(square(fibonacci_generator()), 10):
print(item)

Both programs output the following:

0
1
1
4
9
25
64
169
441
1156

The concepts are the same in both languages. Even though the order in which the operations are written is different: in both cases, the execution order is the same:

  1. We start with the fibonacci sequence/generator.
  2. We apply the square sequence/generator to it.
  3. We take the first 10 items.

In Kotlin the syntax is written from left-to-right, in a chained way, thanks to extension functions, while in Python it’s evaluated from the inside out.

Combinations applied item-by-item

If we add some print statements, we’ll notice something interesting: the calculations of fibonacci and square alternate. We don’t first calculate the first 10 Fibonacci numbers and then apply the square to them. We instead square each Fibonacci number as we go along from the first to the tenth item.

Here are the different sequences/generators with additional print statements:

Kotlin:

fun fibonacciSequence() =
sequence {
var value = 0
var nextValue = 1

while (true) {
println("fib: $value")
yield(value)
value = nextValue.also { nextValue += value }
}
}

fun Sequence<Int>.square() =
sequence {
for (item in iterator()) {
val value = item * item
println("square: $value")
yield(value)
}
}

for (item in fibonacciSequence().square().take(10)) {
println("result: $item")
println()
}

Python:

from typing import Generator, Iterable

def fibonacci_generator() -> Generator[int, None, None]:
value, next_value = 0, 1

while True:
print(f"fib: {value}")
yield value
(value, next_value) = (next_value, value + next_value)

def square(input_iterable: Iterable[int]) -> Generator[int, None, None]:
for item in input_iterable:
value = item * item
print(f"square: {value}")
yield value

for item in islice(square(fibonacci_generator()), 10):
print(f"result: {item}")
print()

Both programs print the following:

fib: 0
square: 0
result: 0

fib: 1
square: 1
result: 1

fib: 1
square: 1
result: 1

fib: 2
square: 4
result: 4

fib: 3
square: 9
result: 9

fib: 5
square: 25
result: 25

fib: 8
square: 64
result: 64

fib: 13
square: 169
result: 169

fib: 21
square: 441
result: 441

fib: 34
square: 1156
result: 1156

The fact that sequence/generator combinations are applied item-by-item is particularly important when the initial source is an infinite one like ours: If the order were instead to start with the fibonacci sequence, complete it, then apply the squares, then take the first 10 items, the program would never terminate.

This behavior is one advantage of using sequences/generators over collections. We’ll dive more into performance trade-offs of sequences versus collections later.

Example 6: cube all items using a shorter syntax

Both Kotlin and Python provide different ways to create sequences/generators. Let’s see a more concise way to create a sequence/generator that cubes all items in an input, using the functional “map” syntax in each language:

Kotlin:

fun Sequence<Int>.cube() = map { it * it * it }

Python:

from typing import Generator, Iterable

def cube(input_iterable: Iterable[int]) -> Generator[int, None, None]:
return (x * x * x for x in input_iterable)

In Kotlin, we use the built-in map method defined on our Sequence object. In Python, we use a generator expression, which is a syntax similar to list comprehensions, using parenthesis instead of square brackets.

Example 7: yieldAll / yield from

Up until now, our sequences/generators have been yielding items one at a time. It is also possible to yield, in one statement, multiple values from another source. We can see an example of this by combining our square and cube functions. Let’s see how to get a Pair/tuple of the square and cube of each Fibonacci number.

Note, for a given Fibonacci number, we’re not going to calculate the square, and then cube that. Instead, we’ll calculate the square and cube separately, and return a Pair/tuple with both values.

Here’s the expected output for the first 10 Fibonacci numbers:

result: (0, 0)
result: (1, 1)
result: (1, 1)
result: (4, 8)
result: (9, 27)
result: (25, 125)
result: (64, 512)
result: (169, 2197)
result: (441, 9261)
result: (1156, 39304)

For this, we’ll create a new sequence/generator function, based on our existing square and cube functions:

Kotlin:

fun Sequence<Int>.squareCube() =
sequence {
yieldAll(square().zip(cube()))
}

Python:

from itertools import tee
from typing import Generator, Iterable

def square_cube(input_iterable: Iterable[int]) -> Generator[int, None, None]:
input1, input2 = tee(input_iterable)
yield from zip(square(input1), cube(input2))

In Kotlin, we use the built-in zip function defined on our Sequence object, and we use it to combine the square and cube sequences. In Python, we also have a built-in zip function that operates on Iterables. However, we see one difference between the two languages here: in Python we have one additional step: we have to first duplicate the input_iterable into two independent Iterables using tee: input1, and input2. If we don’t do this, we’ll have a single instance of our fibonacci generator, advancing twice as fast in its iterations, due to square and cube both calling the same instance separately. This isn’t a problem in Kotlin. The zip function implementation in Kotlin uses a MergingSequence, and we see that the MergingSequence calls iterator() on both provided sequences, creating new Iterators from the sources. This has the same end result as our call to tee() in Python: we use independent iterators over the source sequence/generator.

The execution order here is:

  1. Split the input Iterable into two (explicitly in Python, under the hood in Kotlin).
  2. Create 2 sequence/generators for the input: one to square the items, one to cube the items.
  3. Create a 3rd sequence/iterable using zip. Note that we still haven’t executed any cube or square calculations at this point.
  4. Call yieldAll/yield from to yield each item of the zip sequence/Iterable.

Finally, we can use our “square cube” pairing sequence/generator, and start the execution of the different calculations:

Kotlin:

for (item in fibonacciSequence().squareCube().take(10)) {
println("result: $item")
}

Python:

for item in islice(square_cube(fibonacci_generator()), 10):
print(f"result: {item}")

The output of both programs is as expected:

result: (0, 0)
result: (1, 1)
result: (1, 1)
result: (4, 8)
result: (9, 27)
result: (25, 125)
result: (64, 512)
result: (169, 2197)
result: (441, 9261)
result: (1156, 39304)

Example 8: A more relevant yieldAll/yield from example: “if empty”

Note that our example of getting pairs of squares and cubes of Fibonacci numbers is simple enough that it could be done without yieldAll/yield from, in a more compact syntax:

Kotlin:

val fiboSeq = fibonacciSequence()
for (item in fiboSeq.square().zip(fiboSeq.cube()).take(10)) {
println("result: $item")
}

Python:

for item in islice(
zip(square(fibonacci_generator()), cube(fibonacci_generator())),
10
):
print(f"result: {item}")

So when should we use yieldAll/yield from? I had a hard time coming up with a relevant example in our Fibonacci theme here. But we can find an example in the Kotlin standard library of yieldAll: the sequence function ifEmpty, calls yieldAll on its own iterator if it has data, or yieldAll on a fallback sequence otherwise:

public fun <T> Sequence<T>.ifEmpty(defaultValue: () -> Sequence<T>): Sequence<T> = sequence {
val iterator = this@ifEmpty.iterator()
if (iterator.hasNext()) {
yieldAll(iterator)
} else {
yieldAll(defaultValue())
}
}

An equivalent if_empty function in Python could be as follows:

def if_empty(iterable: Iterable, default_value: Iterable) -> Generator:
try:
first_item = next(iterable)
yield first_item
except StopIteration:
yield from default_value
yield from iterable

Filtering sequences/generators

Suppose we want the cubes of only the odd Fibonacci numbers? We can build on our previous examples, combining our square sequence/generator function with a filter on odd numbers:

Example 9 — Filtering a sequence/generator

Kotlin:

for (item in fibonacciSequence()
.filter { it % 2 == 1 }
.square()
.take(10)) {

println("Odd fibo number cubed: $item")
}

Python:

for item in islice(
square((x for x in fibonacci_generator() if x % 2 == 1)),
10,
):
print(f"Odd fibo number cubed: {item}")

In Kotlin, we use the stdlib filter Sequence extension function, and in Python we use a generator expression with an if condition, to only include odd Fibonacci numbers. We apply the square transformation after the filter.

Once again, we see that the Kotlin code is evaluated from left-to-write, thanks to extension functions, and the Python code is evaluated from the inside-out.

Both programs output the following:

Odd fibo number cubed: 1
Odd fibo number cubed: 1
Odd fibo number cubed: 9
Odd fibo number cubed: 25
Odd fibo number cubed: 169
Odd fibo number cubed: 441
Odd fibo number cubed: 3025
Odd fibo number cubed: 7921
Odd fibo number cubed: 54289
Odd fibo number cubed: 142129

Performance impact of sequences/generators versus collections

We could use lists, instead of sequences or generators, to get pairs of squares and cubes of Fibonacci numbers. Here’s what that would look like:

Example 10 — Use lists instead of sequences/generators

Kotlin:

// 1. Define a function to return a list of Fibonacci numbers
fun fibonacciList(count: Int): List<Int> {
val result = mutableListOf<Int>()
var value = 0
var nextValue = 1
repeat(count) {
result.add(value)
value = nextValue.also { nextValue += value }
}
return result
}
// 2. Define a function return a list of squares of elements in the input list
fun List<Int>.square() = map { it * it }
// 3. Define a function return a list of cubes of elements in the input list
fun List<Int>.cube() = map { it * it * it }

// 4. Create a list of 10 Fibonacci numbers.
val fibo = fibonacciList(10)
// 5. Create a list of the squares of these 10 Fibonacci numbers.
val fiboSquared = fibo.square()
// 6. Create a list of the cubes of these 10 Fibonacci numbers.
val fiboCubed = fibo.cube()
// 7. Zip the squares and cubes into a list of Pairs.
val fiboSquaredCubed = fiboSquared.zip(fiboCubed)

for (item in fiboSquaredCubed) {
println("result: $item")
}

Python:

# 1. Define a function to return a list of Fibonacci numbers
def fibonacci_list(count: int) -> list[int]:
result: list[int] = []
value, next_value = 0, 1

for _ in range(count):
result.append(value)
(value, next_value) = (next_value, value + next_value)
return result

# 2. Define a function return a list of squares of elements in the input list
def square(data: list[int]) -> list[int]:
return [x * x for x in data]

# 3. Define a function return a list of cubes of elements in the input list
def cube(data: list[int]) -> list[int]:
return [x * x * x for x in data]

# 4. Create a list of 10 Fibonacci numbers.
fibo = fibonacci_list(10)
# 5. Create a list of the squares of these 10 Fibonacci numbers.
fibo_squared = square(fibo)
# 6. Create a list of the cubes of these 10 Fibonacci numbers.
fibo_cubed = cube(fibo)
# 7. Zip the squares and cubes into a list of tuples.
fibo_squared_cubed = zip(fibo_squared, fibo_cubed)

# 8. Print the square/cube pairs.
for item in fibo_squared_cubed:
print(f"result: {item}")

1. We create a function to return a finite list of Fibonacci numbers. It has a complexity of O(n), in both processing and memory.

2 and 3. We create functions to square and cube list items, respectively. These functions each return a new list.

4. We call our function to create a list of 10 Fibonacci numbers.

5 and 6. We create two new lists: one with the squares of the Fibonacci numbers, and one with the cubes.

7. We use zip to combine the squared and cubed lists. This also creates a new list.

8. We iterate over our pairs and print them.

With this approach, we have multiple lists, each of 10 elements:

  • The original Fibonacci numbers
  • The squares
  • The cubes
  • The zipped pairs/tuples

If we use sequences/generators instead, we only keep the necessary data (original Fibonacci number, one square, one cube, one zipped pair/tuple) for a single iteration, for each of the 10 Fibonacci numbers. For 10 Fibonacci numbers, this comes out to 4 ints loaded in memory at a time when using sequences/generators, versus 10*4 = 40 ints loaded in memory when using lists.

However, we have a bit of overhead with using sequences: some additional function calls: calling next(), and hasNext() or catching a StopIteration might(?) be more expensive than doing iterations directly on a list.

We have a tradeoff between memory and processing.

Example 11 — More concise syntax using lists

Note that steps 5–8 could be replaced by a more concise syntax, but separating it into the separate steps above helps us better understand that multiple lists are being created.

Here’s what the more concise syntax would look like though:

Kotlin:

for (item in fibo.square().zip(fibo.cube())) {
println("result: $item")
}

Python:

for item in zip(square(fibo), cube(fibo)):
print(f"result: {item}")

Note that this syntax, operating on collections, can look very similar to the syntax used when combining sequence/generator functions, so it may be easy to miss some optimization issues!

Also note one difference between the Kotlin and Python concise syntax examples: in Python, zip will not produce a new list with this concise syntax: it will yield items one by one. But Kotlin’s zip function on a list will create a new list.

Converting between collections, iterators, and sequences

It’s possible to convert between collections, iterators, and sequences. In Kotlin, we have Sequence.iterator(), Sequence.toList(), Collection.asSequence(). In Python, you can convert a generator my_gen to a list with the function list(my_gen), convert a list into an iterator with the iter function, or convert a list into a generator with a generator expression: (x for x in my_list).

Recap

We compared the complexity of calculating the first n Fibonacci numbers using a naive approach versus using iterators and sequences. We compared Kotlin and Python apis for creating iterators and sequences, as a source (generating Fibonacci numbers) and as performing transformations and filtering on other sources (cubing/squaring numbers yielded from a Fibonacci source, including only odd numbers). We also touched on some performance trade-offs when using sequences versus collections.

Cheat sheet of analogous APIs in Kotlin sequences and Python generators

Here is a summary of some concepts/APIs we saw in Kotlin and Python related to sequences/generators.

+-------------+----------------+-----------------------------+
| Kotlin | Python | Concept |
+-------------+----------------+-----------------------------+
| Sequence | Generator | APIs to lazy generate |
| | | items. |
| | | |
| yield() | yield | Yield a single value inside |
| | | a sequence/generator. |
| | | |
| yieldAll() | yield from | Yield multiple values |
| | | inside a sequence/generator |
| | | from another source. |
| | | |
| fun mygen = | def mygen(): | Create a new sequence/ |
| sequence {} | | generator. |
| | | |
| seq.take(5) | islice(seq, 5) | Take the first 5 items |
| | | of a sequence/generator |
| | | |
| seq1.seq2() | seq2(seq1) | Apply seq2 to items |
| | | in seq1. |
| | | |
| seq.map | (f(x) for x | Create a new sequence/ |
| { f(it) } | in seq) | generator with |
| | | function f applied |
| | | to original items. |
| | | |
| seq.filter | (x for x in | Create a new sequence/ |
| { f(it) } | seq if f(x)) | generator with only |
| | | the items which satisfy x. |
| | | |
| - | tee() | Split an iterable into |
| | | two before applying two |
| | | generators to it in |
| | | parallel. |
+-------------+----------------+-----------------------------+

Further exploration — bidirectional generators in Python ↔️

In Python, generators are bidirectional. Instead of simply calling next(generator), it’s possible to call generator.send(some_value). In this case, the generator function implementation can receive the some_value as a “return value” of the yield. It would need a separate article to explore this feature, which doesn’t appear to be present in Kotlin sequences.

A quick example can give a preview of how this works:

Example 11 — Bidirectional generator

def bidirectional_generator():
value_from_send = yield 5
print(f"received {value_from_send}")
yield 7


gen = bidirectional_generator()
produced_by_gen = next(gen)
print(f"produced_by_gen {produced_by_gen}") # produced_by_gen 5
produced_by_gen = gen.send(3) # received 3
print(f"produced_by_gen {produced_by_gen}") # produced_by_gen 7

# outputs:
#
# produced_by_gen 5
# received 3
# produced_by_gen 7

This StackOverflow post shows some more insightful examples of bidirectional generators.

References

--

--