4

Say you have a function that needs to maintain some sort of state and behave differently depending on that state. I am aware of two ways to implement this where the state is stored entirely by the function:

  1. Using a function attribute
  2. Using a mutable default value

Using a slightly modified version of Felix Klings answer to another question, here is an example function that can be used in re.sub() so that only the third match to a regex will be replaced:

Function attribute:

def replace(match):
    replace.c = getattr(replace, "c", 0) + 1
    return repl if replace.c == 3 else match.group(0)

Mutable default value:

def replace(match, c=[0]):
    c[0] += 1
    return repl if c[0] == 3 else match.group(0)

To me the first seems cleaner, but I have seen the second more commonly. Which is preferable and why?

Community
  • 1
  • 1
Andrew Clark
  • 202,379
  • 35
  • 273
  • 306
  • 5
    People just don't think of function attributes because they're not that widely used by beginners, but _everyone_ thinks of using mutable default value because _every_ beginner gets bitten by them at least once. – agf Aug 24 '11 at 23:08

4 Answers4

5

I use closure instead, no side effects.

Here is the example (I've just modified the original example of Felix Klings answer):

def replaceNthWith(n, replacement):
    c = [0]
    def replace(match):
        c[0] += 1
        return replacement if c[0] == n else match.group(0)
    return replace

And the usage:

 # reset state (in our case count, c=0) for each string manipulation
 re.sub(pattern, replaceNthWith(n, replacement), str1)
 re.sub(pattern, replaceNthWith(n, replacement), str2)
 #or persist state between calls
 replace = replaceNthWith(n, replacement)
 re.sub(pattern, replace, str1)
 re.sub(pattern, replace, str2)

For mutable what should happen if somebody call replace(match, c=[])?

For attribute you broke encapsulation (yes i know that python didn't implemented in classes from diff reasons ...)

Community
  • 1
  • 1
Nicolae Dascalu
  • 3,425
  • 2
  • 19
  • 17
2

Both ways feel strange to me. The first though is much better. But when you think about it this way: "Something with a state that can do operations with that state and additional input", it really sounds like a normal object. And when something sounds like an object, it should be an object... SO, my solution would be to use a simple object with a __call__ method:

class StatefulReplace(object):
    def __init__(self, initial_c=0):
        self.c = initial_c
    def __call__(self, match):
        self.c += 1
        return repl if self.c == 3 else match.group(0)

And then you can write in the global space or your module init:

replace = StatefulReplace(0)
xubuntix
  • 2,333
  • 18
  • 19
1

How about:

  1. Use a class
  2. Use a global variable

True, these are not stored entirely within the function. I would probably use a class:

class Replacer(object):
    c = 0
    @staticmethod # if you like
    def replace(match):
        replace.c += 1
        ...

To answer your actual question, use getattr. It's a very clear and readable way to store data away for later. It should be pretty obvious to someone reading it what you're trying to do.

The mutable default argument version is an example of a common programming error (assuming you'll get a new list every time). For that reason alone I would avoid it. Someone reading it later might decide that it's a good idea without fully understanding the consequences. And even in this case, it seems though your function would only work once (your c value is never reset to zero).

Seth
  • 45,033
  • 10
  • 85
  • 120
1

To me, both of these approaches look dodgy. The problem is crying out for a class instance. We don't normally think about functions as maintaining state between calls; that's what classes are for.

That said, I've used function attributes before for this sort of thing. Particularly if it's a one-shot function defined within other code (i.e. not possible for it to be used from anywhere else), just tacking on attributes to it is more concise than defining a whole new class and creating an instance of it.

I would never abuse default values for this. There's a large barrier to understanding because the natural purpose of default values is to provide default values of arguments, not to maintain state between calls. Plus a default argument invites you to supply a non-default value, and you typically get very strange behaviour if you do that with a function that is abusing default values to maintain state.

Ben
  • 68,572
  • 20
  • 126
  • 174