Python 04 | Chaining Numerical Models with Functions and Exception Handling Statements

david 04/12/2025

Introduction

In the previous session, we discussed using control flow statements in Python to achieve batch processing and automation. By repeating calculations with loop statements and differentiating execution paths under various conditions with conditional statements, we can chain together a complex and extensive program.

However, this is an ideal scenario. Repeatedly looping a code block implies that the loop body encompasses all the content we need to execute.

Assuming a certain part of the calculation needs to be reused within the complete program (e.g., similar functionality across multiple loop bodies), we would still need to rewrite that part of the code, leading to increased code redundancy and complexity.

Generally speaking, when we use multi-threading or multi-processing, we can flexibly schedule multiple CPUs to accelerate the computation process. However, this process is typically based on functions, whereas loop statements are overly reliant on single-core CPU performance.

Furthermore, if our program only uses conditional and loop statements, it means we have considered all possible scenarios during program execution and can ensure the program runs without any errors. But for overly large scientific computing models, this also seems unlikely (there’s always an exception).

Considering the above, writing reusable code blocks (i.e., functions) and implementing proper exception handling for the program becomes particularly important. Below, we will explore these concepts simply.

Functions

Such a detested term. Who would have thought, after being tortured by it for so many years in mathematics, we still can’t escape its clutches when learning to code.

But different from its definition in mathematics: we all know that functions in mathematics are one-to-one mapping relationships; multi-valued functions are usually not considered proper functions.

In program design, the definition of a function is broader. It can be any input-output mapping relationship. Inputs and outputs aren’t even required to be numerical values, and the function body isn’t required to have a clear analytical expression like in mathematics (yes, it’s more complicated).

In Python, defining a function only requires the keyword def. By specifying the function name and input parameters, we implement the f(x) part from the mathematical y=f(x) (the x in parentheses is just an example; the input parameters can be any number).

The function body immediately following def is the specific content of f, i.e., how we process the input parameters.

When we finally complete the calculation and obtain our required y (similarly, y can also be any number of output values), we can use the return statement to send back the calculation result. If the result itself is not useful for our subsequent steps, return may not be necessary.

For instance, if we have already calculated the result of a certain module, and that result does not participate in subsequent calculations, and we directly export it to a local file within the function, then we don’t need a return statement.

Here are some simple examples:

python

# Addition
def add(a, b):
    return a + b

print(add(2, 3))

# Define a function to convert Celsius to Fahrenheit or Kelvin
def celsius_to_fahrenheit_or_kelvin(celsius, to_fahrenheit=True):
    if to_fahrenheit:
        return (celsius * 9/5) + 32
    else:
        return celsius + 273.15

print(celsius_to_fahrenheit_or_kelvin(25))
print(celsius_to_fahrenheit_or_kelvin(25, False))

# Define a function to calculate the area and circumference of a circle
import math

def circle_area_and_circumference(radius):
    area = math.pi * radius ** 2
    circumference = 2 * math.pi * radius
    return area, circumference

print(circle_area_and_circumference(5))

# Define a function to determine if a number is prime
def is_prime(n):
    if n <= 1:
        return False
    for i in range(2, int(math.sqrt(n)) + 1):
        if n % i == 0:
            return False
    return True

print(is_prime(1))
print(is_prime(9))
print(is_prime(17))

# Output:
# 5
# 77.0
# 298.15
# (78.53981633974483, 31.41592653589793)
# False
# False
# True

From these examples, we can see the very flexible writing style of Python functions and their free combination with control flow statements. For more traditional programming languages like C, the entire program is essentially a main function. Due to Python’s interactive nature, the definition of a main function is downplayed, and program design is more akin to a command-line style.

It’s worth noting that in the function celsius_to_fahrenheit_or_kelvin, we used the parameter to_fahrenheit, which defaults to True, meaning it defaults to converting Celsius to Fahrenheit. If we need to convert Celsius to Kelvin, we need to pass the parameter to_fahrenheit=False. When we want conversion to Fahrenheit, since the default is True, we can omit this parameter.

Observant friends should notice that when we passed False to convert to Kelvin, we didn’t use the to_fahrenheit=False style, but directly passed the False value. In Python, if we pass parameters in the order defined by the function, we don’t need to write the parameter name; we can pass the value directly.

Furthermore, functions can call and nest within each other. We can encapsulate various components into simple functions and finally combine them to achieve more complex functionality.

python

# Calculate the square of each element in a list
def square(ls):
    return [x**2 for x in ls]

# Calculate the sum of squares of a list
def sum_of_squares(ls):
    return sum(square(ls))

result = sum_of_squares([1, 2, 3, 4, 5])
print(result) # Output: 55

Here, we achieved the calculation of the sum of squares of list elements through nested function calls. In practical geoscience applications, for example, if we need to calculate the four components of radiation to ultimately obtain the total radiation budget, we could define functions for each radiation component separately, then call those functions, and finally calculate the total radiation.

In the square function above, we also used a list comprehension. This expression can simplify loops; we’ll explain it in detail later. Similar cases include conditional comprehensions, dictionary comprehensions, etc.

We can provide an equivalent example to help everyone understand:

python

ls = [1, 2, 3, 4, 5]

# Loop
ls0 = []
for i in ls:
    ls0.append(i ** 2)
print(ls0) # Output: [1, 4, 9, 16, 25]

# List comprehension
ls1 = [i ** 2 for i in ls]
print(ls1) # Output: [1, 4, 9, 16, 25]

Exception Handling

When we finish writing the code for a large computational task, we always hope it can adapt to all usage scenarios. However, things don’t always go as planned; there are always problems of one kind or another.

The most common issues, such as data type errors, differences in file organization, or even memory overflows, can all cause program execution to halt. So, is it possible to first run the overall process to completion and obtain results, ignoring the problematic individuals, and then troubleshoot errors one by one to finish all tasks?

The answer is yes. Python’s exception handling mechanism is designed precisely for this problem.

Overall, the exception handling mechanism is mainly implemented through several keywords:

  • try: Used to define an area where an exception might occur. Within this area, operations that might cause exceptions can be performed. (In short, the main body of our program should be placed within try.)
  • except: Used to handle possible exceptions in the try block. If no exception occurs in the try code, this part is not executed. (Handles possible exceptions in try, which could be skipping the current exception, printing error information, or logging errors, etc.)
  • else: Used to define code that executes when no exception occurs in the try block. (i.e., when the code in try runs normally, execute the code in else.)
  • finally: Used to define code that executes regardless of whether an exception occurred, often used for releasing resources, etc. (No matter what, the code in finally will be executed.)

Among these, try and except must appear in pairs to define the exception handling area, while else and finally are optional.

Here’s a simple example:

python

x = 1

try:
    x = x / 0
except ZeroDivisionError:
    print("division by zero!")
else:
    print("result is", x)
finally:
    del x
    print("clean up.")

# Output:
# division by zero!
# clean up.

In this example, we attempted to execute 1/0, a division by zero operation, which will cause a ZeroDivisionError exception. We used an except statement to catch this exception and print an error message. If no exception occurred, the else statement would execute, printing the result. Finally, the finally statement clears the variable to release resources.

However, here we could anticipate the division by zero exception. Often, we don’t know what exceptions might occur. Therefore, we can use the generic catch-all Exception to catch all exceptions (although this is not recommended, if we truly don’t know what exceptions might occur, we can use this method to see the type of exception).

python

ls = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

for i in ls:
    try:
        y = 10 / i
    except Exception as e:
        print('Error: ', str(e))
    else:
        print(f"The result of 10 / {i} is {y}")
    finally:
        print("This is the finally block")

# Output:
# Error:  division by zero
# This is the finally block
# The result of 10 / 1 is 10.0
# This is the finally block
# The result of 10 / 2 is 5.0
# This is the finally block
# The result of 10 / 3 is 3.3333333333333335
# This is the finally block
# The result of 10 / 4 is 2.5
# This is the finally block
# The result of 10 / 5 is 2.0
# This is the finally block
# The result of 10 / 6 is 1.6666666666666667
# This is the finally block
# The result of 10 / 7 is 1.4285714285714286
# This is the finally block
# The result of 10 / 8 is 1.25
# This is the finally block
# The result of 10 / 9 is 1.1111111111111112
# This is the finally block

We can see that using Exception caught the exception, and the specific content of the exception information is that division by zero caused the error.

Postscript

The above covers the basic content of functions and exception handling in Python; their importance is self-evident.

The status of functions in program design is no less than their status in mathematics. They make code more modular, reusable, and easier to maintain, serving as the cornerstone of programming.

Exception handling is a mechanism that allows a program to automatically handle errors and provide feedback to users when errors occur during execution. It provides a guarantee for the program’s robustness and stability (ruggedness), enabling our program to cope with various abnormal situations.

Functions and exception handling are crucial when we write large programs. When we later mention scenarios like multi-threaded processing, the program’s main body will essentially be a function.

Having learned up to this point, the basic syntax of Python has been generally introduced. Advanced features like class, used when writing our own data types and modules, will be covered later. However, the content so far is sufficient for our daily use.