0

I'm performing an action many times in a loop and want to know how far along I am. I'm trying to make a progress report function that should act something like this:

def make_progress_report(n):
    i = 0
    def progress_report():
        i = i + 1
        if i % n == 0:
            print i
    return progress_report

pr = make_progress_report(2)
pr()
pr()  # 2
pr()
pr()  # 4

This code does not work. Specifically, I get an UnboundLocalError for i. How should I modify it so that it works?

johnsyweb
  • 136,902
  • 23
  • 188
  • 247
jclancy
  • 49,598
  • 5
  • 30
  • 34
  • @delnan Correct, I'd searched around but there are so many questions about closed I didn't read them all. Should I adapt the code from the question you linked and paste it here? – jclancy Mar 17 '13 at 20:53

4 Answers4

4

Here are 3 options:

  1. use a list for your counter:

    def make_progress_report(n):
        i = [0]
        def progress_report():
            i[0] = i[0] + 1
            if i[0] % n == 0:
                print i[0]
        return progress_report
    
  2. use itertools.count to track your counter:

    from itertools import count
    def make_progress_report(n):
        i = count(1)
        def progress_report():
            cur = i.next()
            if cur % n == 0:
                print cur
        return progress_report
    
  3. Use nonlocal for your counter (Python 3+ only!):

    def make_progress_report(n):
        i = 0
        def progress_report():
            nonlocal i
            i = i + 1
            if i % n == 0:
                print i
        return progress_report
    
Mr Fooz
  • 109,094
  • 6
  • 73
  • 101
Gerrat
  • 28,863
  • 9
  • 73
  • 101
  • Is the reason using a list works, as opposed to a number, because lists are mutable? – jclancy Mar 17 '13 at 21:35
  • 3
    Yes...ish. Basically, from within an inner scope, Python doesn't let you re-assign any variables in its outer scope, so you can't directly re-assign `i` to `i+1`. When you modify a list though, the reference to the list remains the same, only its contents change. – Gerrat Mar 17 '13 at 21:40
2

You could consider using a generator:

def progress_report(n):
    i = 0
    while 1:
        i = i+1
        if i % n == 0:
            print i
        yield # continue from here next time

pr = progress_report(2)

next(pr)
next(pr)
next(pr)
next(pr)
mediocrity
  • 437
  • 4
  • 5
0

Look again at how you're defining your closure. The n should be passed in when you define the closure... take the following example:

#!/usr/env python
def progressReportGenerator(n):
    def returnProgress(x):
        if x%n == 0:
            print "progress: %i" % x
    return returnProgress

complete = False
i = 0 # counter
n = 2 # we want a progress report every 2 steps

getProgress = progressReportGenerator(n)

while not complete:
    i+=1 # increment step counter

    # your task code goes here..

    getProgress(i) # how are we going?

    if i == 20: # stop at some arbtirary point... 
        complete = True
        print "task completed"
msturdy
  • 10,479
  • 11
  • 41
  • 52
0

So the progress_report isn't closed over the variable i. You can check it like so...

>>> def make_progress_report(n):
...     i=0
...     def progress_report():
...             i += 1
...             if i % n == 0:
...                     print i
...     return progress_report
...
>>> pr = make_progress_report(2)
>>> pr.__closure__
(<cell at 0x1004a5be8: int object at 0x100311ae0>,)
>>> pr.__closure__[0].cell_contents
2

You'll notice there is only one item in the closure of pr. That is the value you passed in originally for n. The i value isn't part of the closure, so outside of the function definition, i is no longer in scope.

Here is a nice discussion on closures in Python: http://www.shutupandship.com/2012/01/python-closures-explained.html