Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to group functions without side effects?

Tags:

python

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.

  • they take ~15 lines
  • only the main function is called from somewhere else
  • no plans on reusing the helper functions in the near future

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:

  • that's too little code to add a file, especially when there are many files already (this creates a mess)
  • this is an action and now I'm forced to create a noun-based name for the module (or break the modules naming rule). Moreover making it simple will make it too broad (in this case creating a set that really is a singleton).

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:

  • there are no side effects, so there's no need to have the class' instance
  • this is an action and now I'm forced to create a noun-based name for the class (or break the classes naming rule). This leads to many of those "managers" or "creators".
  • this is a misuse of a class concept, this is just a little execution tree with a single function-interface, not a class of things. Misusing concepts slows down understanding and may lead to further blending between uses. I know that in OOP this is common because in some languages you can't really make a function outside of a class, but this is too radical approach to order in code. Objects are useful when they are the closest expression of your idea. This isn't the case. Forcing not fitting order paradoxically generates disorder of a different kind :)

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.

like image 420
Ctrl-C Avatar asked May 09 '18 12:05

Ctrl-C


People also ask

Do pure functional programs have side effects?

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.

How do you stop side effects in Python?

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.

How does functional programming avoid side effects?

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.

How do I avoid side effects in code?

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.


1 Answers

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.)

like image 73
John Kugelman Avatar answered Oct 26 '22 16:10

John Kugelman