Functions#

Don’t repeat yourself#

The DRY principle is one of the most important principles in Software Engineering. Duplicating code increases the chance of introducing errors with every implementation. Moreover, when requirements change, all of the duplicated code section might have to be adjusted, which in turn makes it easy to forget one of the implementations. Therefore, it is important to encapsulate duplicate code sections into functions to make their behavior reusable.

Don’t

my_list = [1, 2, 3, 4] 
with open("log_my_list.txt", "w") as f:
    for num in my_list:
        f.write(str(num) + "\n")

# ...
some_list = [22, 54, 5]
with open("some_list_log.txt", "w") as f:
    for num in some_list:
        f.write(str(num) + "\n")

Do

def write_list_to_file(a_list, path):
    with open(path, "w") as f:
        for num in a_list:
            f.write(str(num) + "\n")

write_list_to_file(my_list, "log_my_list.txt")
# ...
write_list_to_file(some_list, "some_list_log.txt")

One responsibility#

Functions should only be responsible for one single task or, as Robert C. Martin puts it in his Single Responsibility Principle, have only one reason to change.

The following function may look simple, but it is doing several things (or has multiple reasons to change) at once.

Don’t

@dataclass
def TemperatureData:
    datetime: datetime
    value_in_kelvin: float

def write_celsius_values_in_timeframe_to_file(data: list[TemperatureData], start: datetime, end: datetime) -> None:
    celsius_values: list[float]
    for d in data:
        if d.datetime is not None and not math.isnan(d.value_in_kelvin):
            if start <= d.datetime <= end:
                celsius_values.append(d.value_in_kelvin - 273.15)

    with open("logfile.txt", "w") as f:
        f.writelines(celsius_values)

The Integration-Operation-Segregation-Principle#

The IOSP says that a function performs either only integrations or operations. An integration is a function that only calls other integration or operation functions, while an operation is a function that performs low level logic like loops, if statements or calculations. This principle can be a helpful tool to write functions that adhere to the Single Responsibility Principle as functions either only wire an algorithm together or have a single, low level purpose.

Do

def write_celsius_values_in_timeframe_to_log(data: list[TemperatureData], start: datetime, end: datetime) -> None:
    filtered_data = filter(is_valid, data)
    filtered_data = filter(in_timeframe(start, end), filtered_data)
    celsius_values = map(value_in_celsius, filtered_data)
    write_list_to_file(celsius_values, "logfile.txt")

def is_valid(d: TemperatureData) -> bool:
    return d.datetime is not None and not math.isnan(d.value_in_kelvin)

TimeFrameFilter = Callable[[TemperatureData], bool]

def in_timeframe(start: datetime, end: datetime) -> TimeFrameFilter:
    def _in_timeframe(d: TemperatureData) -> bool:
        return start <= d.datetime <= end
    
    return _in_timeframe

def value_in_celsius(data: TemperatureData) -> float:
    return data.value_in_kelvin - 273.15

def write_list_to_file(a_list: list[Any], path: str) -> None:
    with open(path, "w") as f:
        f.writelines(a_list)