Context
Suppose I have the following Python code:
def example_function(numbers, n_iters):
sum_all = 0
for number in numbers:
for _ in range(n_iters):
number = halve(number)
sum_all += number
return sum_all
ns = [1, 3, 12]
print(example_function(ns, 3))
example_function
here is simply going through each of the elements in the ns
list and halving them 3 times, while accumulating the results. The output of running this script is simply:
2.0
Since 1/(2^3)*(1+3+12) = 2.
Now, let's say that (for any reason, perhaps debugging, or logging), I would like to display some type of information about the intermediate steps that the example_function
is taking. Maybe I would then rewrite this function into something like this:
def example_function(numbers, n_iters):
sum_all = 0
for number in numbers:
print('Processing number', number)
for i_iter in range(n_iters):
number = number/2
print(number)
sum_all += number
print('sum_all:', sum_all)
return sum_all
which now, when called with the same arguments as before, outputs the following:
Processing number 1
0.5
0.25
0.125
sum_all: 0.125
Processing number 3
1.5
0.75
0.375
sum_all: 0.5
Processing number 12
6.0
3.0
1.5
sum_all: 2.0
This achieves exactly what I intended. However, this goes a bit against the principle that a function should only do one thing, and now the code for example_function
is sligthly longer and more complex. For such a simple function this is not a problem, but in my context I have quite complicated functions calling each other, and the printing statements often involve more complicated steps than shown here, resulting in a substantial increase in complexity of my code (for one of my functions there were more lines of code related to logging than there were lines related to its actual purpose!).
Furthermore, if I later decide that I don't want any printing statements in my function anymore, I would have to go through example_function
and delete all of the print
statements manually, along with any variables related this functionality, a process which is both tedious and error-prone.
The situation gets even worse if I would like to always have the possibility of printing or not printing during the function execution, leading me to either declaring two extremely similar functions (one with the print
statements, one without), which is terrible for maintaining, or to define something like:
def example_function(numbers, n_iters, debug_mode=False):
sum_all = 0
for number in numbers:
if debug_mode:
print('Processing number', number)
for i_iter in range(n_iters):
number = number/2
if debug_mode:
print(number)
sum_all += number
if debug_mode:
print('sum_all:', sum_all)
return sum_all
which results in a bloated and (hopefully) unnecessarily complicated function, even in the simple case of our example_function
.
Question
Is there a pythonic way to "decouple" the printing functionality from the original functionality of the example_function
?
More generally, is there a pythonic way to decouple optional functionality from a function's main purpose?
What I have tried so far:
The solution I have found at the moment is using callbacks for the decoupling. For instance, one can rewrite the example_function
like this:
def example_function(numbers, n_iters, callback=None):
sum_all = 0
for number in numbers:
for i_iter in range(n_iters):
number = number/2
if callback is not None:
callback(locals())
sum_all += number
return sum_all
and then defining a callback function that performs whichever printing functionality I want:
def print_callback(locals):
print(locals['number'])
and calling example_function
like this:
ns = [1, 3, 12]
example_function(ns, 3, callback=print_callback)
which then outputs:
0.5
0.25
0.125
1.5
0.75
0.375
6.0
3.0
1.5
2.0
This successfully decouples the printing functionality from the base functionality of example_function
. However, the main problem with this approach is that the callback function can only be run at a specific part of the example_function
(in this case right after halving the current number), and all of the printing has to happen exactly there. This sometimes forces the design of the callback function to be quite complicated (and makes some behaviors impossible to achieve).
For instance, if one would like to achieve exactly the same type of printing as I did in a previous part of the question (showing which number is being processed, along with its corresponding halvings) the resulting callback would be:
def complicated_callback(locals):
i_iter = locals['i_iter']
number = locals['number']
if i_iter == 0:
print('Processing number', number*2)
print(number)
if i_iter == locals['n_iters']-1:
print('sum_all:', locals['sum_all']+number)
which results in exactly the same output as before:
Processing number 1.0
0.5
0.25
0.125
sum_all: 0.125
Processing number 3.0
1.5
0.75
0.375
sum_all: 0.5
Processing number 12.0
6.0
3.0
1.5
sum_all: 2.0
but is a pain to write, read, and debug.
from Is there a pythonic way to decouple optional functionality from a function's main purpose?
No comments:
Post a Comment