I have a function with several helper functions. That's fairly common case. I want to group them in a common context for readability and I'm wondering how to do it right.
Simplified example:
def create_filled_template_in_temp(path, values_mapping):
template_text = path.read_text()
filled_template = _fill_template(template_text, values_mapping)
result_path = _save_in_temp(filled_template)
return result_path
def _fill_template(template_text, values_mapping):
...
def _save_in_temp(filled_template):
_, pathname = tempfile.mkstemp(suffix='.ini', text=True)
path = pathlib.Path(pathname)
path.write_text(text)
return path
...
create_filled_template_in_temp(path, values_mapping)
Please note that I don't want the helper methods on the module level because they belong to only one method. Imagine having several such examples as above in the same module. Maany non-public functions on module level. A mess (and this happens many times). Also I'd like to give them context and use the context's name to simplify the naming inside.
Solution #0: A module
Just put it in another module:
template_fillers.create_in_temp(path, values_mapping)
Problems:
Finally this is just too little code to add a module for it.
Solution #1: A class
Create a class with no __init__
and only one public (by naming convention) method:
class TemplateFillerIntoTemp:
def run(self, path, values_mapping):
template_text = path.read_text()
filled_template = self._fill_template(template_text, values_mapping)
result_path = self._save_in_temp(filled_template)
return result_path
def _fill_template(self, template_text, values_mapping):
...
def _save_in_temp(self, filled_template):
_, pathname = tempfile.mkstemp(suffix='.ini', text=True)
path = pathlib.Path(pathname)
path.write_text(text)
return path
...
TemplateFillerIntoTemp().run(path, values_mapping)
This is what I did many times in the past. Problems:
Solution #2: Static class
Take solution #1, add @staticmethod
everywhere. Possibly also ABC metaclass.
TemplateFillerIntoTemp.run(path, values_mapping)
Pro: there is a clear indication that this all is instance-independent. Con: there's more code.
Solution #3: Class with a __call__
Take solution #1, create a __call__
function with the main method, then create on module level a single instance called create_filled_template_in_temp
.
create_filled_template_in_temp(path, values_mapping)
Pro: calls like a single function. Con: implementation is overblown, not really fit for the purpose.
Solution #4: Insert helper functions into main function
Add them inside.
def create_filled_template_in_temp(path, values_mapping):
def _fill_template(template_text, values_mapping):
...
def _save_in_temp(filled_template):
_, pathname = tempfile.mkstemp(suffix='.ini', text=True)
path = pathlib.Path(pathname)
path.write_text(text)
return path
template_text = path.read_text()
filled_template = _fill_template(template_text, values_mapping)
result_path = _save_in_temp(filled_template)
return result_path
...
create_filled_template_in_temp(path, values_mapping)
Pro: this looks well if total number of lines is small and there are very few helper functions. Con: it doesn't otherwise.
Let's define a function for starters. In Layman's terms, Something that either takes zero or more inputs performs some computations and returns an output. Actually, pure functions do the same, but with certain rules. A pure function is deterministic and has no side effects.
The way to avoid using side effects is to use return values instead. Instead of modifying a global variable inside a function, pass the global variable's value in as a parameter, and set that global variable to be equal to a value returned from the function.
Functional programming aims to minimize or eliminate side effects. The lack of side effects makes it easier to do formal verification of a program. The functional language Haskell eliminates side effects such as I/O and other stateful computations by replacing them with monadic actions.
Use pure functions wherever you can. A pure function does not produce side effects. Given the same inputs, a pure function will always return the same output.
Modification of #4: Make inner functions, and also have the function's body be an inner function. This has the nice property of still reading top-to-bottom, rather than having the body all the way at the bottom.
def create_filled_template_in_temp(path, values_mapping):
def body():
template_text = path.read_text()
filled_template = fill_template(template_text, values_mapping)
result_path = save_in_temp(filled_template)
return result_path
def fill_template(template_text, values_mapping):
...
def save_in_temp(filled_template):
_, pathname = tempfile.mkstemp(suffix='.ini', text=True)
path = pathlib.Path(pathname)
path.write_text(text)
return path
return body()
(I don't care for the leading underscores, so they didn't survive.)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With