[Tutor] question about metaclasses

Steven D'Aprano steve at pearwood.info
Sat Jan 13 20:09:00 EST 2018


On Wed, Jan 10, 2018 at 07:29:58PM +0100, Peter Otten wrote:

[...]
>             elif not isinstance(obj, property):
>                 attrs[attr] = property(lambda self, obj=obj: obj)

> PS: If you don't remember why the obj=obj is necessary:
> Python uses late binding; without that trick all lambda functions would 
> return the value bound to the obj name when the for loop has completed.

This is true, but I think your terminology is misleading. For default 
values to function parameters, Python uses *early* binding, not late 
binding: the default value is computed once at the time the function is 
created, not each time it is needed.

So in this case, each of those property objects use a function that sets 
the default value of obj to the current value of obj at the time that 
the property is created.

Without the obj=obj parameter, Python creates a *closure*. A closure is 
a computer-science term for something like a snap shot of the 
environment where the function was created.

If we had written this instead:

    attrs[attr] = property(lambda self: obj)

the name "obj" doesn't refer to a local variable of the lambda function. 
Nor does it refer to a global variable, or a builtin function. It refers 
to a "non-local variable": it belongs to the function that surrounds the 
lambda function, not the lambda itself.

And so Python would create a *closure* for the lambda function so that 
when it eventually gets called, it knows where to find the value of obj.

And *that* process, of looking up the value of obj from a closure, uses 
late binding: all the lambda functions will refer to the same 
environment, which means they will also see the same value for obj.

Namely the last value obj received when the outer function (the one 
that the closure refers back to) completed.

Here's another example to show the difference. Rather than use lambda, 
I'm going to use regular "def" to prove that this has nothing to do with 
lambda itself, the rules apply every time you create a function.

Start with the closure version:

# --- cut here %< ---

def factory_closure():
    # Create many new functions, each of which refer to i in its
    # enclosing scope.
    functions = []
    for i in range(5):
        def f():
            return i  # << this i is a NONLOCAL variable
        functions.append(f)
    return functions

functions = factory_closure()

# All the closures refer to the same thing.
for f in functions:
    print(f.__closure__)

# And the functions all see the same value for i
print([f() for f in functions])

# --- cut here %< ---


And here is a version which avoids the closure issue by using the 
function parameter default value trick:


# --- cut here %< ---

def factory_no_closure():
    # Create many new functions, each of which refer to i using
    # a parameter default value.
    functions = []
    for i in range(5):
        def f(i=i):
            return i  # << this i is a LOCAL variable
        functions.append(f)
    return functions

functions = factory_no_closure()

# None of the functions need a closure.
for g in functions:
    print(g.__closure__)

# And the functions all see different values for i
print([g() for g in functions])

# --- cut here %< ---


In practice, this is generally only an issue when single invocation of a 
factory function creates two or more functions at once, and that 
generally means inside a loop:


def factory():
    for i in something:
        create function referring to i


If your factory only returns one function at a time, like this:


def factory(i):
    create function referring to i

for n in something:
    factory(n)


then each function still uses a closure, but they are *different* 
closures because each one is created on a different invocation of the 
factory. That's another way to avoid this "early/late binding" problem.


-- 
Steve


More information about the Tutor mailing list