Notes about How Closure in Python 3 Captures Variables

Just 2 notes about how closure in Python 3 captures variables.

Note 1.

The wrong way

def make_multipiler_the_wrong_way():
    multipilers = []
    # Tries to remember each i
    for i in range(5):
        # All remember same last i
        multipilers.append(lambda x: i * x) 
    return multipilers

if __name__ == "__main__":
    m = make_multipiler_the_wrong_way()
    for i in range(5):
        print(m[i](3))

The output is

12
12
12
12
12

So the right way should be

def make_multipiler_the_right_way():
    def make_lambda(i):
        # when the closure is made
        # i is captured/bound in the scope of `make_lambda(i)`
        # so it won't change anymore
        return lambda x: i * x

    multipilers = []
    for i in range(5):
        multipilers.append(make_lambda(i))
    return multipilers

if __name__ == "__main__":
    m = make_multipiler_the_right_way()
    for i in range(5):
        print(m[i](3))

And it outputs what we desired

0
3
6
9
12

Why? See locals() between these 2 versions

def make_multipiler_the_wrong_way():
    multipilers = []
    # Tries to remember each i
    for i in range(5):
        # All remember same last i
        multipilers.append(lambda x: print("i: {}/({}), x: {}, local: {}".format(i, hex(id(i)), x, locals()))) 
    return multipilers

if __name__ == "__main__":
    m = make_multipiler_the_wrong_way()
    for i in range(5):
        print(m[i](3))

The locals() in the wrong version

i: 4/(0x1003f5f70), x: 3, local: {'x': 3, 'i': 4}
i: 4/(0x1003f5f70), x: 3, local: {'x': 3, 'i': 4}
i: 4/(0x1003f5f70), x: 3, local: {'x': 3, 'i': 4}
i: 4/(0x1003f5f70), x: 3, local: {'x': 3, 'i': 4}
i: 4/(0x1003f5f70), x: 3, local: {'x': 3, 'i': 4}

As you can see, the captured variable i points to a same address. That's the problem.

As for the right one

def make_multipiler_the_right_way():
    def make_lambda(i):
        # when the closure is made
        # i is captured/bound in the scope of `make_lambda(j)`
        # so it won't change anymore
        return lambda x: print("i: {}/({}), x: {}, local: {}".format(i, hex(id(i)), x, locals()))

    multipilers = []
    for i in range(5):
        multipilers.append(make_lambda(i))
    return multipilers

if __name__ == "__main__":
    m = make_multipiler_the_right_way()
    for i in range(5):
        print(m[i](3))

Its locals go as below

i: 0/(0x108bf1ef0), x: 3, local: {'x': 3, 'i': 0}
i: 1/(0x108bf1f10), x: 3, local: {'x': 3, 'i': 1}
i: 2/(0x108bf1f30), x: 3, local: {'x': 3, 'i': 2}
i: 3/(0x108bf1f50), x: 3, local: {'x': 3, 'i': 3}
i: 4/(0x108bf1f70), x: 3, local: {'x': 3, 'i': 4}

The underlying memory addresses for each i in these closures are different.

Note 2.

The wrong way goes

def counter():
    # init count to 0
    count = 0
    
    # the closure
    def add_one():
        # increase count by 1
        count += 1
        # return the result
        return count
    
    # return the closure
    return add_one

if __name__ == "__main__":
    count_whatever = counter()
    print(count_whatever())
    print(count_whatever())
    print(count_whatever())

However, this code won't compile

Traceback (most recent call last):
  File "counter.py", line 17, in <module>
    print(count_whatever())
  File "counter.py", line 8, in add_one
    count += 1
UnboundLocalError: local variable 'count' referenced before assignment

As the error message suggests, variable count in the closure add_one() is a local variable, which implies that the closure didn't capture the variable count in counter()

The right way is

def counter():
    # init count to 0
    count = 0
    
    # the closure
    def add_one():
        # declare that `count` is a nonlocal variable
        nonlocal count
        # increase count by 1
        count += 1
        # return the result
        return count
    
    # return the closure
    return add_one

if __name__ == "__main__":
    count_whatever = counter()
    print(count_whatever())
    print(count_whatever())
    print(count_whatever())

Now we have the expected output

1
2
3

Why?

Because objects of built-in types like (int, float, bool, str, tuple, unicode, frozenset) are immutable in Python.

The value of them didn't changed, not really. The fact is that the memory address the variable referred to has been changed.

>>> num = 1
>>> hex(id(num))
'0x10159ef10'
>>> num += 1
>>> hex(id(num))
'0x10159ef30'
def counter():
    # init count to 0
    count = 0
    s = "1"
    # the closure
    def add_one():
        # declare that `count` is a nonlocal variable
        nonlocal count
        # increase count by 1
        count += 1
        # debug
        print("count: {}/({}), local: {}".format(count, hex(id(count)), locals()))
        # return the result
        return count
    
    # return the closure
    return add_one

if __name__ == "__main__":
    count_whatever = counter()
    print(count_whatever())
    print(count_whatever())
    print(count_whatever())

The code above outputs

count: 1/(0x10397cf10), local: {'count': 1}
1
count: 2/(0x10397cf30), local: {'count': 2}
2
count: 3/(0x10397cf50), local: {'count': 3}
3

声明: 本文为0xBBC原创, 转载注明出处喵~

Leave a Reply

Your email address will not be published. Required fields are marked *