Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

python counter with closure

I'm trying to build a counter in python with the property of closure. The code in the following works:

def generate_counter():
    CNT = [0]
    def add_one():
        CNT[0] = CNT[0] + 1
        return CNT[0]
    return add_one

However when I change the list CNT to a var, it did not work:

def generate_counter1():
    x = 0
    def add_one():
        x = x + 1
        return x
    return add_one

when I print the closure property of an instance, I found the __closure__ of the second case is none:

>>> ct1 = generate_counter()
>>> ct2 = generate_counter1()
>>> print(ct1.__closure__[0])
<cell at 0xb723765c: list object at 0xb724370c>
>>> print(ct2.__closure__)
None

Just wondering why the index in outer function has to be a list?


Thanks for the answers! Found the docs which clearly explained this problem https://docs.python.org/3/faq/programming.html#why-am-i-getting-an-unboundlocalerror-when-the-variable-has-a-value

like image 964
Vince Wang Avatar asked Dec 05 '22 00:12

Vince Wang


1 Answers

Python determines the scope of a name by looking at name binding behaviour; assignment is one such behaviour (function parameters, importing, the target in for target ... or while .. as target are other examples). A name you bind to in a function is considered local. See the Naming and Binding section of the reference documentation.

So the name x in your second example is a local variable, because you assigned directly to it:

x = x + 1

In fact, because you never gave that x a local value, you'll get an exception when you try to use that function; the local name is unbound when you try to read it:

>>> generate_counter1()()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in add_one
UnboundLocalError: local variable 'x' referenced before assignment

In your first example no such binding takes place; you are instead altering the contents of CNT, what that name references is not altered.

If you are using Python 3, you can override the decision to make a name local, by using a nonlocal statement:

def generate_counter2():
    x = 0
    def add_one():
        nonlocal x
        x = x + 1
        return x
    return add_one

By making x non-local, Python finds it in the parent context and creates a closure for it again.

>>> def generate_counter2():
...     x = 0
...     def add_one():
...         nonlocal x
...         x = x + 1
...         return x
...     return add_one
...
>>> generate_counter2().__closure__
(<cell at 0x1078c62e8: int object at 0x1072c8070>,)

nonlocal is new in Python 3; in Python 2 you are limited to tricks like using a mutable list object to evade the binding rule. Another trick would be to assign the counter to an attribute of the nested function; again, this avoids binding a name in the current scope:

def generate_counter3():
    def add_one():
        add_one.x += 1
        return add_one.x
    add_one.x = 0
    return add_one
like image 56
Martijn Pieters Avatar answered Dec 30 '22 18:12

Martijn Pieters