8

An oft-asked question is whether there is an equivalent to static variables inside functions in Python. There are many answers, such as creating wrapper classes, using nested functions, decorators, etc.

One of the most elegant solutions I found was this, which I have slightly modified:

def foo():
    # see if foo.counter already exists
    try: test = foo.counter
    # if not, initialize it to whatever
    except AttributeError: foo.counter = 0

    # do stuff with foo.counter
    .....
    .....

Example:

static.py

def foo(x):
    # see if foo.counter already exists
    try: test = foo.counter
    # if not, initialize it to whatever
    except AttributeError: foo.counter = 0

    foo.counter += x

    print(foo.counter)

for i in range(10):
    foo(i)

output

$ python static.py
0
1
3
6
10
15
21
28
36
45

Is there any reason I should avoid this method? How the heck does it work, anyway?

Community
  • 1
  • 1
abalter
  • 9,663
  • 17
  • 90
  • 145
  • 2
    Just curious, why would you want to do this? Isn't it why python has generators? – xthestreams Apr 01 '16 at 04:13
  • 1
    Most of the examples I see of generators are for simple incrementing procedures. Also, they introduce another level of indentation. I'm not trying to `yeild` a number, I just want to be able to go back to the function and have my last value stored. In my case, it is a max value of some stuff that is passed in. Various operations ensue. – abalter Apr 01 '16 at 04:15
  • What's wrong with the decorator solution on the page you linked to? Regardless, all the solutions on the page you linked to look fine. – 101 Apr 01 '16 at 04:16
  • The decorator solution requires defining two functions. Lots of extra page space, and less "like" a static variable declaration. This is all just taste and semantics, EXCEPT for if there is something evil underlying this method that could cause problems. I feel unsure, since I don't really understand how it works in the first place. – abalter Apr 01 '16 at 04:17
  • 1
    I can't see anything at all objectively evil with what you've shown. – 101 Apr 01 '16 at 04:19
  • @101 well, objectively it is a global, so there's that. – Peter Wood Apr 01 '16 at 05:00
  • 1
    This is a classic singleton pattern and should be avoided if you ever want to run in a multi threaded environment because it will eventually fail subtly. Since the function is an object, one might as well use a proper class that's easily understood. – Nick Apr 01 '16 at 16:42
  • 1
    IDK. Class seems like overkill. Create a class just so I an instantiate one object to do something that in other languages takes one line? Why would it fail in a multithreaded environment? – abalter Apr 01 '16 at 20:02
  • Why are there two votes to close? – abalter Apr 01 '16 at 20:02
  • Not sure if the question on votes to close was rhetorical, but it's now 4, and all are because some folks felt this was too opinion oriented. – Foon Apr 02 '16 at 11:43
  • 1
    I don't see how it is opinion oriented. The idea that a method could lead to conflicts such as was pointed out in terms of scope is a technical programming question, not an opinion. I wanted to use that method with confidence that wouldn't introduce vulnerability to hard to debug errors and such. That is not opinion. – abalter Apr 04 '16 at 06:49

6 Answers6

2

How does this work?

It works because the function's name is just another entry in the local scope, and the function is an object like everything else in Python, and can have arbitrary attributes set on it:

def foo():
    # The foo function object has already been fully constructed
    # by the time we get into our `try`
    try: test = foo.counter  # Only run when foo is invoked
    except AttributeError: foo.counter = 0

    foo.counter += 1

if hasattr(foo, 'counter'):
    print('Foo has a counter attribute')
else:
    # This prints out - we've parsed `foo` but not executed it yet
    print('Foo.counter does not exist yet')

# Now, we invoke foo
foo()

if hasattr(foo, 'counter'):
    # And from now on (until we remove the attribute)
    # this test will always succeed because we've added the counter
    # attribute to the function foo.
    print('Foo has a counter attribute')
else:
    print('Foo.counter does not exist yet')  # No longer true
Sean Vieira
  • 155,703
  • 32
  • 311
  • 293
  • 1
    Thanks for the explanation. And, I think you have answered my question. A function is just an object, and all I did was add an attribute. What strikes me as odd is that I'm adding the attribute _inside_ the function itself. I would not have guessed that `foo` was in scope _inside_ of `foo`. – abalter Apr 01 '16 at 04:28
  • 3
    @abalter: It has to be in scope. Otherwise recursion wouldn't work. – user2357112 Apr 01 '16 at 04:38
  • 3
    `def foo` defines the name `foo` in the scope of the module. Everything in the module is in scope inside the function `foo` including the name `foo`. – Peter Wood Apr 01 '16 at 05:02
2

In python, you would probably be much better served by using generator functions.

  1. It enables multiple simultaneous scopes (each generator instance can have it's own instance of foo.counter).
  2. The "static" variables are properly encapsulated within the scope of the function (foo.counter is actually in the outer scope (file-level scope)).

Here's an example of using two simultaneous generators, each with their own version of the counter variable (not possible with "static" variables).

def foo():
    counter = 0
    while True:
        # You can yield None here if you don't want any value.
        yield counter
        counter += 1

gen1 = foo()
gen2 = foo()
gen1.next()
# 0
gen1.next()
# 1
gen2.next()
# 0

You can provide some initial values to the generator and also send data back into the generators as well.

def foo(x=0)
    counter = x
    val = 1
    while True:
        sent = (yield counter)
        if sent is None:
            counter += val
            val = 1
        else:
            val = sent

gen1 = foo(3)
gen1.next()
# 3
gen1.send(3)
gen1.next()
# 6
gen1.next()
# 7

You can do much more than simply iterate a counter. Generators are a powerful tool in python and are much more flexible that simple "static" variables.

Brendan Abel
  • 35,343
  • 14
  • 88
  • 118
2

I feel like an object is exactly what you're looking for here. It's a bit of state attached to some actions (in this case one action) that use and manipulate that state. So why not:

class Foo(object):
    def __init__(self, start=0):
        self.counter = start
    def foo(self, x):
        self.counter += x
        print(self.counter)
foo = Foo()
for i in range(10):
    foo.foo(i)

As others have stated, if you really really want to avoid a class you can. A function is already an object and can have any attribute added to it, just like any ordinary object. But why would you really want that? I understand that writing a class for a single function feels a bit like overkill, but you have stated that your actual code has various operations that ensue. Without seeing the various operations and such, it does seem like you have a reasonable case for using a class here.

1

Why not this:

def foo(x):
    foo.counter += x
    print(foo.counter)

foo.counter = 0 # init on module import

And then:

for i in range(10):
    foo(i)

I get the same output with py2.7, py3.4.

Nick
  • 2,342
  • 28
  • 25
  • 1
    That solution is also given elsewhere, and is roughly equivalent. I just think it is safer and neater to keep the initialization inside the function. More safely reusable. Just a personal opinion. The question is: is there any reason this type of solution should NOT be used? The issue is adding an attribute to `foo` within `foo`. That strikes me as odd, and I don't even understand exactly what is going on. – abalter Apr 01 '16 at 04:13
  • One should profile it, the try block may drop the performance a bit. – Nick Apr 01 '16 at 16:28
1

You might run into some issues was the test will look outside the function scope for a foo.counter if it doesn't find one in the function. Eg the following returns 101 instead of 1

class Bar():
    counter = 100


class Hoo():
    def foo(x):
    # see if foo.counter already exists
        try: test = foo.counter
    # if not, initialize it to whatever
        except AttributeError: foo.counter = 0
        foo.counter += x
        print(foo.counter)
# make an object called foo that has an attribute counter    
foo = Bar()
# call the static foo function    
Hoo.foo(1)
xthestreams
  • 169
  • 5
  • Very strange. I need to think about that when I'm less tired. Isn't that sort of thing always a possibility in late-binding languages where you can clobber attributes and methods? – abalter Apr 01 '16 at 06:11
  • Interesting idea, but did you actually run it (and if so, which python) When I run as written in python2, I get TypeError: unbound method foo() must be called with Hoo instance as first argument (got int instance instead). Changing it so we have an outer function (to hide foo so doing foo = Bar() doesn't overwrite) wrap_foo which returns the inner function and calling that, I get 1. It doesn't make sense to me that python would try to look up foo.counter repeatedly – Foon Apr 02 '16 at 11:40
0

The solution you have works fine, but if you're after the most elegant solution you may prefer this (adapted from one of the answers you linked to):

def foo(x):
    foo.counter = getattr(foo, 'counter', 0) + x
    print(foo.counter)

for i in range(10):
    foo(i)

It works essentially the same, but getattr returns a default value (of 0) that only applies if foo doesn't have the counter attribute.

101
  • 8,514
  • 6
  • 43
  • 69
  • Interesting. Visually cleaner. Is there added overhead from calling the `getattr` method over the `try`/`catch`? That was pointed out as a reason to not use the equivalently simple `if`/`then`. – abalter Apr 01 '16 at 04:34
  • @abalter: The difference is minimal and not very noteworthy. Using `try/catch` is slightly faster when the attribute is present, using `getattr()` is slightly faster when it is not. – Dietrich Epp Apr 01 '16 at 04:58
  • I guess in practice, `try/catch` wins because if you actually use the function more than once, the attribute will be there. But "slightly" is probably not an issue for most applications. – abalter Apr 01 '16 at 06:13
  • 1
    Yep, but I think you're overthinking it all a bit ;) – 101 Apr 01 '16 at 06:31