How to group functions without side effects?

  • A+
Category:Languages

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.


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

Comment

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: