11

I am trying to write a python function that on the first call, returns a 1. On the second call, returns a 2. On the third, a 3. Etc.

Currently, I have achieved this using a global variable:

index = 0

def foo():
    global index
    index += 1
    return index

When calling the function three times:

print(foo())
print(foo())
print(foo())

It returns the values expected:

1
2
3

But, I've read that it is bad practice to use global variables. So, I was wondering if the same result could be achieved without using globals.

Any suggestion?

Thank you for your help.

martineau
  • 119,623
  • 25
  • 170
  • 301
manocormen
  • 747
  • 1
  • 6
  • 17

8 Answers8

12

Using a closure:

def make_inc():
    val = [0]
    def inc():
        val[0] += 1
        return val[0]
    return inc

inc = make_inc()
print inc()
print inc()
print inc()

Using a class (the most obvious solution in an OOPL ):

class Inc(object):
    def __init__(self):
        self._val = 0

    def __call__(self):
        self._val += 1
        return self._val


inc = Inc()
print inc()
print inc()
print inc()

Using a generator (not directly callable, you'll have to use the .next() method):

def incgen():
    val = 0
    while True:
        val += 1
        yield val


inc = incgen()
print inc.next()
print inc.next()
print inc.next()
bruno desthuilliers
  • 75,974
  • 6
  • 88
  • 118
7

You can use function attributes:

def f():
    f.counter = getattr(f, 'counter', 0) + 1
    return f.counter

Or closures:

def w():
    counter = 0
    def f():
        nonlocal counter
        counter += 1
        return counter
    return f
Sergey Gornostaev
  • 7,596
  • 3
  • 27
  • 39
  • `nonlocal` is Python3 only IIRC. wrt/ to the "function attribute" trick, I would definitly not recommand it, as it's really brittle (add `g = f; f = lambda : None` after `f` definition and you'll find out why). – bruno desthuilliers Jul 19 '16 at 10:21
4

I will provide an alternative solution to Sergey's answer: exploit mutable default arguments!

def f(_=[0]):
    _[0] += 1
    return _[0]

This has the disadvantage that a user might incorrectly call the function with one argument and it will not receive an error message.

Community
  • 1
  • 1
Bakuriu
  • 98,325
  • 22
  • 197
  • 231
2

Can you use an object?

class Incrementer:
    def __init__(self):
        self.value = 0

    def __call__(self):
        print(self.value)
        self.value += 1

Then you can call it like a function after instantiating it:

>>> a = Incrementer()
>>> a()
0
>>>a()
1
p-robot
  • 4,652
  • 2
  • 29
  • 38
2

You can use generator also.

def yrange(n):
    i = 0
    while i < n:
        yield i
        i += 1

Output:

>>> y = yrange(3)
>>> y
<generator object yrange at 0x401f30>
>>> y.next()
0
>>> y.next()
1
>>> y.next()
2
>>> y.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
Dinesh Pundkar
  • 4,160
  • 1
  • 23
  • 37
0

You can also improve generator, make it more flexible:

def counter_gen():
    count = 0
    while True:
        res = yield count
        count += res

>>> counter = counter_gen()
>>> counter.send(None) #initialize generator
>>> 0
>>> counter.send(1)
>>> 1
>>> counter.send(3)
>>> 4  
Ivan Semochkin
  • 8,649
  • 3
  • 43
  • 75
0

Besides what @bruno desthuilliers made in his first "closure solving", i would like to point, it is just ok to use a list instead of doing some closures.

What i mean is, it will work fine, if you modify it this way.

index = [0]

def foo():
    index[0] = index[0] + 1
    return index[0]

we used a list rather than using a simple variable, it doesn't try to create another list named index, because to create a list you need to something like this,

anotherlist= []

or

anotherlist= list(())

why a problem happens when you use a simple variable is,

the script attempts to make an assignment to the variable index, which creates a new name in the local scope. Then, when Python executes the right-hand side of the assignment it finds the name index in the local scope with nothing assigned to it yet.

0
foo = lambda x=[0]: (x.__setitem__(0, (x[0] + 1)) or x[0])
Julien REINAULD
  • 599
  • 2
  • 5
  • 18