Python - Coroutines



Python Coroutines are a fundamental concept in programming that extend the capabilities of traditional functions. They are particularly useful for asynchronous programming and complex data processing pipelines.

Coroutines are an extension of the concept of functions and generators. They are designed to perform cooperative multitasking and manage asynchronous operations.

In traditional functions i.e. subroutines which have a single entry and exit point where as coroutines can pause and resume their execution at various points by making them highly flexible.

Key Characteristics of Coroutines

Following are the key characteristics of Coroutines in python −

  • Multiple Entry Points: Coroutines are not constrained to a single entry point like traditional functions. They can pause their execution at certain points, when they hit a yield statement and resume later. This allows coroutines to handle complex workflows that involve waiting for or processing asynchronous data.
  • No Central Coordinator: When we see traditional functions i.e subroutines, which are often coordinated by a main function but coroutines operate more independently. They can interact with each other in a pipeline fashion where data flows through a series of coroutines, each performing a different task.
  • Cooperative Multitasking: Coroutines enable cooperative multitasking. This means that instead of relying on the operating system or runtime to switch between tasks the programmer controls when coroutines yield and resume by allowing for more fine-grained control over execution flow.

Subroutines vs Coroutines

Subroutines are the traditional functions with a single entry point and no inherent mechanism for pausing or resuming execution. They are called in a defined sequence and handle tasks with straightforward control flow.

Coroutines are the advanced functions with multiple entry points that can pause and resume their execution. They are useful for tasks that require asynchronous execution, complex control flows and data pipelines. They support cooperative multitasking by allowing the programmer to control when execution switches between tasks.

The following table helps in understanding the key differences and similarities between subroutines and coroutines by making us easier to grasp their respective roles and functionalities in programming.

Aspect Subroutines Coroutines
Definition A sequence of instructions performing a task. A generalization of subroutines that can pause and resume execution.
Entry Points Single entry point. Multiple entry points; can pause and resume execution.
Execution Control Called by a main function or control structure. These can suspend execution and be resumed later and programmer controls switching.
Purpose Perform a specific task or computation. Manage asynchronous operations, cooperative multitasking and complex workflows.
Calling Mechanism Typically called by a main function or other subroutines. Invoked and controlled using 'next()', 'send()', and 'close()' methods.
Data Handling No built-in mechanism for handling data exchanges; typically uses parameters and return values. Can receive and process data using 'yield' with 'send()'.
State Management No inherent mechanism to maintain state between calls. Maintains execution state between suspensions and can resume from where it left off.
Usage These are used for modularizing code into manageable chunks. These are used for asynchronous programming, managing data pipelines and cooperative multitasking.
Concurrency Not inherently designed for concurrent execution; typically used in sequential programming. Supports cooperative multitasking and can work with asynchronous tasks.
Example Usage Helper functions, utility functions. Data pipelines, asynchronous tasks, cooperative multitasking.
Control Flow Execution follows a linear path through the code. Execution can jump back and forth between coroutines based on yield points.

Execution of Coroutines

Coroutines are initiated with the __next__() method which starts the coroutine and advances execution to the first yield statement. The coroutine then waits for a value to be sent to it. The send() method is used to send values to the coroutine which can then process these values and potentially yield results.

Basic Coroutine Example

A coroutine uses the yield statement which can both send and receive values. Unlike a generator which yields values for iteration where as a coroutine typically uses yield to receive input and perform actions based on that input. Following is the basic example of the Python coroutine −

def print_name(prefix):
    print(f"Searching prefix: {prefix}")
    while True:
        name = (yield)
        if prefix in name:
            print(name)

# Instantiate the coroutine
corou = print_name("Welcome to")

# Start the coroutine
corou.__next__()

# Send values to the coroutine
corou.send("Tutorialspoint")
corou.send("Welcome to Tutorialspoint")

Output

Searching prefix: Welcome to
Welcome to Tutorialspoint

Closing a Coroutine

Coroutines can run indefinitely so it's important to close them properly when they are no longer needed. The close() method terminates the coroutine and handles cleanup. If we attempt to send data to a closed coroutine it will raise a StopIteration exception.

Example

Following is the example of closing a coroutine in python −

def print_name(prefix):
    print(f"Searching prefix: {prefix}")
    try:
        while True:
            name = (yield)
            if prefix in name:
                print(name)
    except GeneratorExit:
        print("Closing coroutine!!")

# Instantiate and start the coroutine
corou = print_name("Come")
corou.__next__()

# Send values to the coroutine
corou.send("Come back Thank You")
corou.send("Thank you")

# Close the coroutine
corou.close()

Output

Searching prefix: Come
Come back Thank You
Closing coroutine!!

Chaining Coroutines for Pipelines

Coroutines can be chained together to form a processing pipeline which allows data to flow through a series of stages. This is particularly useful for processing sequences of data in stages where each stage performs a specific task.

Example

Below is the example which shows chaining coroutines for pipelines −

def producer(sentence, next_coroutine):
   '''
   Splits the input sentence into tokens and sends them to the next coroutine.
   '''
   tokens = sentence.split(" ")
   for token in tokens:
      next_coroutine.send(token)
   next_coroutine.close()

def pattern_filter(pattern="ing", next_coroutine=None):
   '''
   Filters tokens based on the specified pattern and sends matching tokens to the next coroutine.
   '''
   print(f"Searching for {pattern}")
   try:
      while True:
         token = (yield)
         if pattern in token:
            next_coroutine.send(token)
   except GeneratorExit:
      print("Done with filtering!!")
      next_coroutine.close()

def print_token():
   '''
   Receives tokens and prints them.
   '''
   print("I'm the sink, I'll print tokens")
   try:
      while True:
         token = (yield)
         print(token)
   except GeneratorExit:
      print("Done with printing!")

# Setting up the pipeline
pt = print_token()
pt.__next__()

pf = pattern_filter(next_coroutine=pt)
pf.__next__()

sentence = "Tutorialspoint is welcoming you to learn and succeed in Career!!!"
producer(sentence, pf)

Output

I'm the sink, I'll print tokens
Searching for ing
welcoming
Done with filtering!!
Done with printing!
Advertisements