20

I'm not referring to closures, where the outer function returns the inner function, or to memoization, specifically. There have been a few instances where I've wanted to write a recursive function, perhaps with memoization, and it seemed much simpler to initialize a dictionary or some other data structures in the outer function, and then have a recursive helper function writing to and accessing the dict and the arguments of the outer function. Here's what I mean --

def foo(arr, l):
   cache = {}
   result = []

   def recursive_foo_helper(i, j, k):
      # some code that depends on arr, l, i, j, k, cache, and result

   for (x, y) in arr:
      if recursive_foo_helper(x, y, k):
         return result
   return None

instead of having the helper function declared separately with some super long signature like,

recursive_foo_helper(arr, l, i, j, k, cache={}, result=[])

I have read that doing this is pretty standard for memoization, but I was curious if there's a consensus on whether it's OK to do just for recursive helper functions.

1
  • If you are not calling the function any other places I can't see why this would be a bad thing. Commented Jun 10, 2013 at 22:36

6 Answers 6

13

There are a lot of good reasons. Personally, I often use nested functions to keep the namespace clean. It's especially useful within object methods :

class Foo(object):
    def bar(self):
        def baz(val):
            return val
        return [ baz(i) for i in range(1,101) ]

If I declare baz outside of bar, I either need to make it a method of Foo, or expose it to the entire package.

Sign up to request clarification or add additional context in comments.

Comments

8

I use a nested function to find matches from a list:

def get_exact(data, key, match):
    def is_match(item):
        if (key in item) and (item[key].lower() == match.lower()):
            return item
        return False
    return [i for i in data if is_match(i)]

No other calls in the project have a need to use is_match(item), so why declare it separately?

However, I will say that, for my example, declaring is_match() outside of get_exact() does run around ~0.04 seconds faster in 10,000 iterations.

def is_match(item, key, match):
    if (key in item) and (item[key].lower() == match.lower()):
        return item
    return False

def get_exact(data, key, match):
    return [i for i in data if is_match(i, key, match)]

Comments

4

I usually use closures, however the other way you suggest (what I sometimes call a Wrapper) is also pretty useful and in my personal experience it works fine. I've never had someone tell me to avoid that style. If your code works and is readable (which I think it is) then go for it!

Comments

1

I'd say using a closure as you suggest is cleaner, but really needs the nonlocal keyword from Python3 if you want to rebind references. For mutable objects it's obviously ok to modify them.

In the meantime, it's common to see the defaultarg hacks/idioms in Python2

One (maybe the only) disadvantage of the nested function is that it's difficult to unittest.

To make sure the cache can't grow unbounded, you also need a way to remove items (eg least recently used)

Comments

1

One disadvantage of nested functions is when it comes to testing.

def compute_sequence(sequence):
    """Compute blabla on sequence"""

    def compute_element(element):
        """Compute blabla on element"""
        return element * 2

    sequence = sequence + 1  # global treatment
    return [compute_element(element) for element in sequence]

Questions: How do you test compute_element ? How do you access its docstring ?

Opinion: If you check "import this" you'll see:

The Zen of Python, by Tim Peters [...] Flat is better than nested. [...]

Even if I massively used nested functions today I feel unconfortable using them Peace out

Comments

0

I think nested functions are great for recursion. Consider the following function for searching if a BST contains a node with the data target:

def contains_target(self, target) -> bool:
        def _contains_target(node, target):
            if node is not None:
                found = False
                found = _contains_target(node.left, target)
                if found:
                    return True
                found = _contains_target(node.right, target)
                if found:
                    return True
                found = target == node.data
                return found
            else:
                return False

        # Begin at the root
        return _contains_target(self.root, target)

I found this to be preferable as you can have an initialized argument for one or all of the parameters when the embedded helper function is first ran. In this case, the first node considered was the root node of the tree.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.