[Tutor] Generator next()

Steven D'Aprano steve at pearwood.info
Sun Dec 29 13:12:14 CET 2013


On Sun, Dec 29, 2013 at 01:57:31AM -0500, Keith Winston wrote:

> Also: in the timer function, it has a series of attribute assignments to
> 0/None after the inner function definition... from the behaviour, I assume
> those are applied once, the first time the timer function is called
> wrapping a new method/function, and then not again, but that doesn't really
> make sense. Again, I don't really have a well-formed question, maybe that's
> clear enough?

Here's the code in question, stripped to the absolutely bare minimum, 
and with a change of name:

def decorator(function):
    def inner(*args, **kwargs):
        result = function(*args, **kwargs)
        inner.count += 1
        return result
    inner.count = 0
    return inner


Let's see what happens when you call "decorator" with some function as 
argument:

def spam(n):
    return "spam"*n

f = decorator(spam)

What does this function "f()" do? Let's go through the process step by 
step.

When we call decorator(spam), we enter the decorator() function with the 
local variable "function" set to the argument spam. The next thing that 
happens is that decorator() creates a new function called "inner". 
Inside "inner", stuff happens, but we don't care about that yet. (We'll 
come back to that in a moment.) The "inner" function is created, and 
then decorator() adds a new attribute to it:

    inner.count = 0

Functions, being objects, can have attributes. We can add our own. We 
use this attribute to keep track of how many times the function gets 
called.

Then decorator() returns this newly created function object, complete 
with its attribute count=0. This new function object is assigned to the 
variable "f".

What happens when we call f()?

Here's the code for "f" a.k.a. "inner", again, stripped of the outer 
layer:

    def inner(*args, **kwargs):
        result = function(*args, **kwargs)
        inner.count += 1
        return result

Notice that this is a *closure* (remember them from my previous email?). 
There are two non-local variables inside this inner function, one called 
"function" and one called "inner". "function" is the original spam 
function, the argument passed to the outer "decorator" function. "inner" 
is also a closure: it is the inner function itself. So inside the 
"inner" function, we have five variables:

- local variable "args", which collects positional arguments;

- local variable "kwargs", which collects keyword arguments;

- non-local variable "function", set to spam (the argument to the
  outer "decorator" function);

- local variable "result", set to the result of calling "function"
  with the collected arguments;

- and finally non-local variable "inner", which is set to the 
  inner function itself.


So calling f() collects all the arguments (if any) into parameters args 
and kwargs. Then spam gets called with those arguments, and the result 
saved. Then the count attribute is incremented, and finally the result 
returned.

All this can be a bit tricky to keep track of, because the one function 
can be known by different names depending on where in the code you are 
looking. Outside of the decorator function, we have a function called 
"spam". Inside the decorator, it is known as "function". Inside the 
decorator, there is a nested function called "inner". Outside of the 
decorator, it is known as "f".

We say that f (a.k.a. inner) is a wrapper function around spam. 
Alternatively, we say that f "decorates" spam. The special decorator 
syntax:

@decorator
def spam(n):
    return "spam"*n


is a short-cut for this:

def spam(n):
    return "spam"*n

spam = decorator(spam)


This may be a lot to digest in one sitting. Factory functions, 
decorators, closures, they're all fantastically useful and powerful 
features of Python, but they can be a bit tricky for people to wrap 
their brains around.

[more to come]



-- 
Steven


More information about the Tutor mailing list