2018年6月10日 星期日

[ Python 文章收集 ] Closures in Python 3

Source From Here 
Preface 
In this article I will introduce you to closures in Python 3. Actually, a closure is a function in Python which allows you to do things that aren’t possible in another language (like C/C++ or Java) with functions. In Python everything is treated like a first-class citizen, so they can be passed around just as normal variables. So are functions too. You probably have seen code like this already: 
  1. >>> def adder(a, b):  
  2. ...     return a + b  
  3. ...  
  4. >>> def caller(fun):  
  5. ...     print(fun(24))  
  6. ...     print(fun(35))  
  7. ...  
  8. >>> caller(adder)  
  9. 6  
  10. 8  
In this example above we created a function caller which calls another function which is passed to it as an argument. 

Because we can pass around functions as arguments we can return functions too from function calls. Let's consider the following example: 
  1. >>> def contains_factory(x):  
  2. ...     def contains(lst):  
  3. ...         return x in lst  
  4. ...     return contains  
  5. ...  
  6. >>> contains_15 = contains_factory(15)  
After running this example, contains_15 has the type of a function: 
.contains at 0x101d78b70>

And these functions are closures

What makes closures special? 
If you think that closures are ordinary functions you might have missed the key difference from the previous example. 

Call the contains_15 with some lists / iterables to test the functionality: 
  1. >>> contains_15([1,2,3,4,5])  
  2. False  
  3. >>> contains_15([1314151617])  
  4. True  
  5. >>> contains_15(range(120))  
  6. True  
The key point of closures are that they remember their context in which they were created. In the example above contains_15 remembers, that the contains_factory function was called with the value 15. And this 15 is used for later in the contains function as the variable x when you provide multiple iterables to look-up x in them. 

Another interesting fact is, that the closure remains existing even if the original creator function (in the example case it is contains_factoryis deleted
  1. >>> del contains_factory  
  2. >>> contains_factory(42)  
  3. Traceback (most recent call last):  
  4.   File "", line 1, in   
  5. NameError: name 'contains_factory' is not defined  
  6. >>> contains_15(range(1420))  
  7. True  
How to create a closure? 
After this introduction on closures you should know how to create closures in Python. But for the sake of brevity let's sum things up: 
1. We have to create a nested function (a function inside another function).
2. This nested function has to refer to a variable defined inside the enclosing function.
3. The enclosing function has to return the nested function

Pretty simple, isn't it? The second point is simple in reality, optional -- but if you do not reference a variable from the enclosing function there is not much sense to create a nested function and return it -- you simply define a function. You could do this in the normal scope too. 

Let's create another interesting closure: a counter. The idea behind the counter is that in some cases you just want to count interactions. In those cases you define a global variable (most of the time called counter) and increment it at the right place when an interaction occurs. We can replace this global variable and the incrementation by defining a closure and call this closure every time we want to count something: 
  1. >>> def counter_factory():  
  2. ...     count = 0 # here I create the variable to increment  
  3. ...     def counter():  
  4. ...         nonlocal count  
  5. ...         count += 1  
  6. ...         return count  
  7. ...     return counter  
  8. ...  
The example above creates a function which will generate a counter closure every time it is invoked -- and the counter starts from 0 and increases every time you call the closure. 
  1. >>> counter1 = counter_factory()  
  2. >>> counter1()  
  3. 1  
  4. >>> counter1()  
  5. 2  
  6. >>> counter2 = counter_factory()  
  7. >>> counter2()  
  8. 1  
  9. >>> counter1()  
  10. 3  
  11. >>> counter2()  
  12. 2  
Well, this solution is not quite what we want because we return the value of count every time with the function invocation. This means that if we want to verify that there were no interactions then we have to do something like this: 
  1. counter1() == 1  
And this requires thinking and remembering. And we are humans that make errors. Fortunately there is a solution for this problem. An enclosing function has no restriction on the number of returned closures. So to fix this issue we can create two closures: one will increment the count variable, the other will return the current value of the count variable: 
- test.py 
  1. def counter_factory():  
  2.     count = [0]  # This variable is created to do increment  
  3.   
  4.     def counter():  
  5.         count[0] += 1  
  6.         return count[0]  
  7.   
  8.     def current():  
  9.         return count[0]  
  10.   
  11.     return (counter, current)  
Now we have access to two closures and we can use them as we want: 
>>> incrementer, getter = counter_factory()
>>> getter

>>> incrementer

>>> getter()
0
>>> getter()
0
>>> incrementer()
1
>>> incrementer()
2
>>> incrementer()
3
>>> getter()
3
>>> getter()
3
>>> incrementer, getter = counter_factory()
>>> getter()
0
>>> incrementer()
1

As you can see, the incrementer increments silently the value of count inside the closure. getter returns the actual value of count without incrementing it -- and this makes the feature usable. Resetting the counter is done by simply re-assigning the closures. 

What are closures good for? 
Even if closures seem pretty interesting (a function returning another function which knows its creation context!) there is another question: where can we utilize closures to make the best of them? 

Here are a few uses for closures: 
* Eliminating global variables
* Replacing hard-coded constants
* Providing consistent function signatures

Late binding of closures 
A source of problems is the way Python binds the variables in closures. For example you write: 
  1. def create_adders():  
  2.     adders = []  
  3.     for i in range(10):  
  4.         def adder(x):  
  5.             return i + x  
  6.         adders.append(adder)  
  7.   
  8.     return adders  
  9.   
  10.   
  11. for adder in create_adders():  
  12.     print(adder(1))  
You may expect the following output: 
2
3
4
5
6
7
8
9
10
11

In reality you get: 
10
10
10
10
10
10
10
10
10
10

What happens? You create 10 functions which add 1 to the number 9. This is because of the late-binding of closures: the variables used inside the closures are looked up at the time the inner function is called. Whenever the adder function is called the inner loop over the range(10) is finished and the variable i has the value 9 -- so you end up with 10 for each 10 functions as result. 

To solve this problem you could add an additional parameter to your closures which will maintain the right state
  1. def create_adders():  
  2.     adders = []  
  3.     for i in range(10):  
  4.         def adder(x, i=i):  # Use parameter i to hold the value during iteration  
  5.             return i + x  
  6.         adders.append(adder)  
  7.   
  8.     return adders  
Here we added i as a local variable to the closure with a default value of i. This later i comes from the range and sets the value of the closure i to 0 to 9 respectively. This solves the problem if we run the example. 

Conclusion 
We have seen that closures are functions which remember the context (lexical scope) in which they were created and they can use it even if the program flow is no longer in the enclosing scope. To create a closure you need to write a function which returns another function. The returned function is the closure itself. 

Closure variables are late binding, so take care when you use them: you may end up with unwanted results! 

Supplement 
Python Closures and the Python 2.7 nonlocal Solution

沒有留言:

張貼留言

[Git 常見問題] error: The following untracked working tree files would be overwritten by merge

  Source From  Here 方案1: // x -----删除忽略文件已经对 git 来说不识别的文件 // d -----删除未被添加到 git 的路径中的文件 // f -----强制运行 #   git clean -d -fx 方案2: 今天在服务器上  gi...