Skip to main content

Python: Modifying a Counter in a Closure

I helped a fellow Pythonista with a problem today, and it reminded me that a lot of people might not know about the trick of using a list to box variables within a closure. If that doesn't make sense, read the code below:
def counter(start):

"""Create a counter starting at ``start``."""

# The value "curr" needs to be wrapped in a list. Otherwise, when
# "+=" is used in "inc", Python binds the variable at that scope
# instead of at this scope. We don't want to redefine a variable at
# the inner scope. We want to modify the variable at this outer
# scope. Java programmers would call this "boxing", but they would
# use an Integer instance instead of a list.

curr = [start]

def inc():
"""Increment the counter and return the new value."""
curr[0] += 1
return curr[0]

return inc


if __name__ == '__main__':
c = counter(5)
for i in range(3):
print c() # Prints 6 7 8.

Comments

PJE said…
There's a better way, from a readability point of view. Replace "curr = [start]" with "inc.count = start", and move it after the "def inc():" block. Now replace all uses of "curr[0]" with "inc.count".

Now your code will be understandable even to the uninitiated. :)
jjinux said…
Nice :)
dan said…
I've been using that array idiom for a while now but it always seemed a bit icky to me. Never thought of using a function attribute to do the job - it looks a lot nicer.
Anonymous said…
def inc(count=[0]):
count[0] += 1
return count[0]
Dirkjan Ochtman said…
I'm kind of surprised that PJE's trick works... How is it that there's a variable "inc" bound to the function within the function itself? Apparently, that would be always the case, but I don't see how that makes sense. It's impossible to declare something in its OWN scope, right?
Anonymous said…
I don't get it. What makes this superior to:

def counter(start, stop):
while start < stop:
yield start
start += 1

for i in counter(5,10):
print i

# prints 5 6 7 8 9

or even

for i in [i + 5 for i in range(3)]:
print i
# prints 5 6 7

?
Anonymous said…
"How is it that there's a variable "inc" bound to the function within the function itself?"

"inc.count" is an attribute on the function object, not a variable inside the function.
Alex said…
You can use this trick to translate the established technique for making private data in Javascript into Python.

class Foo:
  def __init__(self):
     private_vars = [ 0, "Alex" ]

     def count(): return private_vars[0]
     self.count = count

     def set_count(c): private_vars[0] = c
     self.set_count = set_count

     def name(): return private_vars[1]
     self.name = name

     def set_name(n): private_vars[1] = n
     self.set_name = set_name

Now you can do this:

f = Foo()
f.set_name("someone else...")
jjinux said…
> def inc(count=[0]):
> count[0] += 1
> return count[0]

Unfortunately, this provides only a single global counter, which is insufficient. The other versions create a new counter for each closure.

Mutable static defaults--ugly ;)
jjinux said…
> I'm kind of surprised that PJE's trick works...

Functions are objects. With the exception of some immutable objects like strings, you can attach stuff to any object.
jjinux said…
> I don't get it. What makes this superior to:

Sorry, you missed my point. You are absolutely right that generators are better mechanisms to create counters. I wasn't really trying to create a counter. I was trying to show how to modify a lexically scoped, non-local variable using the list trick.
jjinux said…
> You can use this trick to translate the established technique for making private data in Javascript into Python.

Nice! I hadn't seen that :)
Anonymous said…
> Functions are objects. With the exception of some immutable objects like strings, you can attach stuff to any object.

I understand that I can attach attributes to any function. What I don't understand is that the function object is available in the function body when the function evaluation hasn't been completed yet (in the function body).
jjinux said…
> I understand that I can attach attributes to any function. What I don't understand is that the function object is available in the function body when the function evaluation hasn't been completed yet (in the function body).

Oh, now I see your confusion. You are correct that you cannot attach function attributes (that I know of) before the function definition is done. PJE was saying you can attach attributes to the inner function from the outer function.
Anonymous said…
But that is EXACTLY what he's saying: "replace all uses of 'curr[0]' with 'inc.count'. The uses of curr[0] are all *inside* inc(). And it works, too, I tested it in the interactive interpreter:

>>> def inc():
... inc.count += 1
... return inc.count
...
>>> inc.count = 0
>>> inc()
1
>>> inc()
2

Hmm, that makes sense, actually. By the time the function body is executed, the inc() function is fully defined in the global namespace, so now the function body can access the link to the function object in the global scope.
jjinux said…
> Hmm, that makes sense, actually.

Ah, yes, you are correct. Very good. It's not really "global" of course, but I think you've nailed it.

Popular posts from this blog

Drawing Sierpinski's Triangle in Minecraft Using Python

In his keynote at PyCon, Eben Upton, the Executive Director of the Rasberry Pi Foundation, mentioned that not only has Minecraft been ported to the Rasberry Pi, but you can even control it with Python. Since four of my kids are avid Minecraft fans, I figured this might be a good time to teach them to program using Python. So I started yesterday with the goal of programming something cool for Minecraft and then showing it off at the San Francisco Python Meetup in the evening.

The first problem that I faced was that I didn't have a Rasberry Pi. You can't hack Minecraft by just installing the Minecraft client. Speaking of which, I didn't have the Minecraft client installed either ;) My kids always play it on their Nexus 7s. I found an open source Minecraft server called Bukkit that "provides the means to extend the popular Minecraft multiplayer server." Then I found a plugin called RaspberryJuice that implements a subset of the Minecraft Pi modding API for Bukkit s…

Apple: iPad and Emacs

Someone asked my boss's buddy Art Medlar if he was going to buy an iPad. He said, "I figure as soon as it runs Emacs, that will be the sign to buy." I think he was just trying to be funny, but his statement is actually fairly profound.

It's well known that submitting iPhone and iPad applications for sale on Apple's store is a huge pain--even if they're free and open source. Apple is acting as a gatekeeper for what is and isn't allowed on your device. I heard that Apple would never allow a scripting language to be installed on your iPad because it would allow end users to run code that they hadn't verified. (I don't have a reference for this, but if you do, please post it below.) Emacs is mostly written in Emacs Lisp. Per Apple's policy, I don't think it'll ever be possible to run Emacs on the iPad.

Emacs was written by Richard Stallman, and it practically defines the Free Software movement (in a manner of speaking at least). Stal…

Creating Windows 10 Boot Media for a Lenovo Thinkpad T410 Using Only a Mac and a Linux Machine

TL;DR: Giovanni and I struggled trying to get Windows 10 installed on the Lenovo Thinkpad T410. We struggled a lot trying to create the installation media because we only had a Mac and a Linux machine to work with. Everytime we tried to boot the USB thumb drive, it just showed us a blinking cursor. At the end, we finally realized that Windows 10 wasn't supported on this laptop :-/I've heard that it took Thomas Edison 100 tries to figure out the right material to use as a lightbulb filament. Well, I'm no Thomas Edison, but I thought it might be noteworthy to document our attempts at getting it to boot off a USB thumb drive:Download the ISO. Attempt 1: Use Etcher. Etcher says it doesn't work for Windows. Attempt 2: Use Boot Camp Assistant. It doesn't have that feature anymore. Attempt 3: Use Disk Utility on a Mac. Erase a USB thumb drive: Format: ExFAT Scheme: GUID Partition Map Mount the ISO. Copy everything from the I…