- Python Shortcuts, Commands, and Packages
- 4.2 Twenty-Two Programming Shortcuts
- 4.3 Running Python from the Command Line
- 4.4 Writing and Using Doc Strings
- 4.5 Importing Packages
- 4.6 A Guided Tour of Python Packages
- 4.7函数作为一类对象
- 4.8 Variable-Length Argument Lists
- 4.9 Decorators and Function Profilers
- 4.10 Generators
- 4.11 Accessing Command-Line Arguments
- Chapter 4 Summary
- Chapter 4 Questions for Review
- Chapter 4 Suggested Problems
4.9 Decorators and Function Profilers
When you start refining your Python programs, one of the most useful things to do is to time how fast individual functions run. You might want to know how many seconds and fractions of a second elapse while your program executes a function generating a thousand random numbers.
Decorated functions can profile the speed of your code, as well as provide other information, because functions are first-class objects. Central to the concept of decoration is awrapperfunction, which does everything the original function does but also adds other statements to be executed.
Here’s an example, illustrated byFigure 4.3. The decorator takes a function F1 as input and returns another function, F2, as output. This second function, F2, is produced by including a call to F1 but adding other statements as well. F2 is a wrapper function.
Figure 4.3.How decorators work (high-level view)
Here’s an example of a decorator function that takes a function as argument and wraps it by adding calls to thetime.timefunction. Note thattimeis a package, and it must be imported beforetime.timeis called.
import time def make_timer(func): def wrapper(): t1 = time.time() ret_val = func() t2 = time.time() print('Time elapsed was', t2 - t1) return ret_val return wrapper
There are several functions involved with this simple example (which, by the way, is not yet complete!), so let’s review.
There is a function to be given as input; let’s call this theoriginalfunction (F1 in this case). We’d like to be able to input any function we want, and have it decorated—that is, acquire some additional statements.
Thewrapperfunction is the result of adding these additional statements to the original function. In this case, these added statements report the number of seconds the original function took to execute.
Thedecoratoris the function that performs the work of creating the wrapper function and returning it. The decorator is able to do this because it internally uses thedefkeyword to define a new function.
Ultimately, the wrapped version is intended to replace the original version, as you’ll see in this section. This is done by reassigning the function name.
If you look at this decorator function, you should notice it has an important omission: The arguments to the original function,func, are ignored. The wrapper function, as a result, will not correctly callfuncif arguments are involved.
The solution involves the*argsand**kwargslanguage features, introduced in the previous section. Here’s the full decorator:
import time def make_timer(func): def wrapper(*args, **kwargs): t1 = time.time() ret_val = func(*args, **kwargs) t2 = time.time() print('Time elapsed was', t2 - t1) return ret_val return wrapper
The new function, remember, will bewrapper. It iswrapper(or rather, the function temporarily namedwrapper) that will eventually be called in place offunc; this wrapper function therefore must be able to take any number of arguments, including any number of keyword arguments. The correct action is to pass along all these arguments to the original function,func. Here’s how:
ret_val = func(*args, **kwargs)
Returning a value is also handled here; the wrapper returns the same value asfunc, as it should. What iffuncreturns no value? That’s not a problem, because Python functions returnNoneby default. So the valueNone, in that case, is simply passed along. (You don’t have to test for the existence of a return value; there always is one!)
Having defined this decorator,make_timer, we can take any function and produce a wrapped version of it. Then—and this is almost the final trick—we reassign the function name so that it refers to the wrapped version of the function.
def count_nums(n): for i in range(n): for j in range(1000): pass count_nums = make_timer(count_nums)
The wrapper function produced bymake_timeris defined as follows (except that the identifierfuncwill be reassigned, as you’ll see in a moment).
def wrapper(*args, **kwargs): t1 = time.time() ret_val = func(*args, **kwargs) t2 = time.time() print('Time elapsed was', t2 - t1) return ret_val
We now reassign the namecount_numsso that it refers tothisfunction—wrapper—which will call the originalcount_numsfunction but also does other things.
Confused yet? Admittedly, it’s a brain twister at first. But all that’s going on is that (1) a more elaborate version of the original function is being created at run time, and (2) this more elaborate version is what the name,count_nums, will hereafter refer to. Python symbols can refer to any object, including functions (callable objects). Therefore, we can reassign function names all we want.
count_nums = wrapper
Or, more accurately,
count_nums = make_timer(count_nums)
So now, when you runcount_nums(which now refers to the wrapped version of the function), you’ll get output like this, reporting execution time in seconds.
>>>count_nums(33000)Time elapsed was 1.063697338104248
The original version ofcount_numsdid nothing except do some counting; this wrapped version reports the passage of time in addition to calling the original version ofcount_nums.
在最后一个年代tep, Python provides a small but convenient bit of syntax to automate the reassignment of the function name.
@decoratordeffunc(args):statements
This syntax is translated into the following:
deffunc(args):statementsfunc=decorator(func)
In either case, it’s assumed thatdecoratoris a function that has already been defined. This decorator must take a function as its argument and return a wrapped version of the function. Assuming all this has been done correctly, here’s a complete example utilizing the@sign.
@make_timer def count_nums(n): for i in range(n): for j in range(1000): pass
After this definition is executed by Python,count_numcan then be called, and it will executecount_numas defined, but it will also add (as part of the wrapper) aprintstatement telling the number of elapsed seconds.
Remember that this part of the trick (the final trick, actually) is to get the namecount_numsto refer to thenewversion ofcount_nums后,增加了新的声明the process of decoration.