I am learning Python using Dive Into Python 3 book. I like it, but I don't understand the example used to introduce Closures in Section 6.5.
I mean, I see how it works, and I think it's really cool. But I don't see any real benefit: it seems to me the same result could be achieved by simply reading in the rules file line by line in a loop, and doing search / replace for each line read.
Could someone help me to:
either understand why using closures in this example improves the code (e.g., easier to maintain, extend, reuse, or debug?)
or suggest a source of some other real-life code examples where closures really shine?
Thank you!
Decorators are an example of closures. For example,
def decorate(f):
def wrapped_function():
print("Function is being called")
f()
print("Function call is finished")
return wrapped_function
@decorate
def my_function():
print("Hello world")
my_function()
The function wrapped_function
is a closure, because it retains access to the variables in its scope--in particular, the parameter f, the original function. Closures are what allow you to access it.
Closures also allow you to retain state across calls of a function, without having to resort to a class:
def make_counter():
next_value = 0
def return_next_value():
nonlocal next_value
val = next_value
next_value += 1
return val
return return_next_value
my_first_counter = make_counter()
my_second_counter = make_counter()
print(my_first_counter())
print(my_second_counter())
print(my_first_counter())
print(my_second_counter())
print(my_first_counter())
print(my_second_counter())
Also, bound methods are technically closures (though they're probably implemented differently). Bound methods are class member functions with their class baked in:
import sys
w = sys.stdout.write
w("Hello\n")
w
is essentially a closure with a reference to the sys.stdout
object.
Finally, I haven't read that book, but a quick read of the chapter you linked and I'm very unimpressed--it's so horribly roundabout that it's useless as an explanation of closures.
This might not seem particularly useful when you have access to the entire code base or when you do not have reusability in mind, but it is incredibly powerful and useful when trying to separate out logic into different, reusable modules that can be implemented in parallel by different developers. If you were to simply read the pattern strings from the file, every single module would have to be aware of this file and pass around that annoying list of pattern strings. And if you changed your system so that the pattern strings came from a URL instead of from a file, it could completely break your entire codebase. On the other hand, if you processing logic simply takes a callback function or several call back functions, and then you have another module that constructs the functions dynamically using content from a file, then only the component that constructs the functions needs to change. That is the power of being able to create functions dynamically.
here's an closure usage, of get configures:
def confmaker():
cf=ini_conf()
def clo(*args):
return cf.get(*args)
return clo
cfget=confmaker()
cfget(...)
here ini_conf is called only once. In my understanding, closures avoid global variables(like cf), and make usage simple.
Niels-Bom writes (with edits):
The same result could be achieved by simply reading in the rules file line by line in a loop, and doing search / replace for each line read.
And, in fact, this is what is essentially accomplished in section 6.5 where the rules are placed within the file plural4-rules.txt. With rules now as strings they can be maintained within a file, and our code separates data from control. This makes the project easier to manage and maintain.
Niels' question motivated me to sketch out the development of chapter 6 in an effort to understand exactly what the author was trying to demonstrate. There are many lessons to learn in the development provided and it is not just about closures, but about best practices in coding as well.
The exercise helped me understand how generators can be used to replace alternative, less abstract, and more enmeshed implementations. The development of the material from 6.2 through 6.6 is educational enough to sketch out here.
So I begin with Niels' second point concerning the breaking up of the rules into separate functions in 6.3 as a segue into the sketch:
Why does using closures in this example improve the code?
The author states at this point:
Was adding this level of abstraction worth it? Well, not yet.
There is still sections 6.4-6.6 to work through. The story of closures, and in this case, building a pluralization generator is achieved step by step, beginning with the hard coded rules in a module called plural(noun). So, beginning with the first relevant section and summarizing our way through to end of the chapter we have the following.
6.2 Let's Use Regular Expressions: Here the author takes the opportunity to reinforce and extend our understanding of regular expressions with pluralization rules hard coded within the initial plural function.
6.3. A List of Functions: Abstracts the rules hard coded within the plural function to several stand alone functions. This is a "stepping stone" to the next section. But it also demonstrates that there is an important difference between the usage of match_sxz() and match_sxz.
6.4 A List Of Patterns: The fact that we created individual named functions, paired as match and apply, in 6.3 are redundant. These functions are all based on the same pattern and are never called directly. Here he modifies this code to make it simpler to change the rules. This becomes a further level of abstraction, with the rules now specified as strings within the variable called pattern. The pluralization rules are no longer functions.
6.5 A File Of Patterns: With no more duplicate code and the pluralization rules defined in a list of strings, the next step toward building the generator is to put these strings in a separate file. Here they become more maintainable, separated from the code that uses them.
6.6 Generators: The generator is a generic plural() function that parses the rules file, checks for a match, applies the rule if appropriate, and goes to next rule. This is an example of a closure.
That’s all the plural() function has to do, and that’s all the plural() function should do.
A relatively simple and beautiful development, sophisticated enough to be useful, and extensible to other types of problems one might find, especially in text pattern recognition.
The author addresses performance concerns of this particular solution at the end of the tutorial. Opening and reading lines from a file will degrade performance, especially with an increasing number of open() calls. He states that better performance can be attained using an iterator, which is considered later in the book.
reading in the rules file line by line in a loop
line by line in a loop
loop
This will drive performance through the floor. Read once, apply many times.