Here is presented a way to identify if a function is a closure or not via code objects.
As already mentioned in other answers, not every nested function is a closure. Given a composite function (which represent the overall action) its intermediate states can be either be a closure or a nested function.
A closure is a kind function which is "parametrized" by its (non-empty) enclosing scope, the space of free-variables. Notice that a composite function may be made by both types.
The (Python's) internal type code
object represents the compiled function body. Its attribute co_cellvars and co_freevars can be used to "lookaround" the closure/scope of a function.
As mentioned in the doc
co_freevars: tuple of names of free variables (referenced via a function’s closure)
co_cellvars: tuple of names of cell variables (referenced by containing scopes).
Once the function is read, by performing recursive calls a partial function is returned with its own __closure__ (hence cell_contents) and a list of free-variables from its clousre and in its scope.
Let introduce some support functions
# the "lookarounds"
def free_vars_from_closure_of(f):
print(f.__name__, 'free vars from its closure', f.__code__.co_cellvars)
def free_vars_in_scopes_of(f):
print(f.__name__, 'free vars in its scope ', f.__code__.co_freevars)
# read cells values
def cell_content(f):
if f.__closure__ is not None:
if len(f.__closure__) == 1: # otherwise problem with join
c = f.__closure__[0].cell_contents
else:
c = ','.join(str(c.cell_contents) for c in f.__closure__)
else:
c = None
print(f'cells of {f.__name__}: {c}')
Here an example from another answer rewritten in a more systematic way
def f1(x1):
def f2(x2):
a = 'free' # <- better choose different identifier to avoid confusion
def f3(x3):
return '%s %s %s %s' % (x1, x2, a, x3)
return f3
return f2
# partial functions
p1 = f1('I')
p2 = p1('am')
# lookaround
for p in (f1, p1, p2):
free_vars_in_scopes_of(p)
free_vars_from_closure_of(p)
cell_content(p)
Output
f1 free vars in its scope () # <- because it's the most outer function
f1 free vars from its closure ('x1',)
cells of f1: None
f2 free vars in its scope ('x1',)
f2 free vars from its closure ('a', 'x2')
cells of f2: I
f3 free vars in its scope ('a', 'x1', 'x2')
f3 free vars from its closure () # <- because it's the most inner function
cells of f3: free, I, am
The lambda counterpart:
def g1(x1):
return lambda x2, a='free': lambda x3: '%s %s %s %s' % (x1, x2, a, x3)
From the point of view of the free variables/scoping are equivalent. The only minor differences are some values of some attributes of the code object:
co_varnames, co_consts, co_code, co_lnotab, co_stacksize... and natuarlly the __name__ attribute.
A mixed example, closures and not at once:
# example: counter
def h1(): # <- not a closure
c = 0
def h2(c=c): # <- not a closure
def h3(x): # <- closure
def h4(): # <- closure
nonlocal c
c += 1
print(c)
return h4
return h3
return h2
# partial functions
p1 = h1()
p2 = p1()
p3 = p2('X')
p1() # do nothing
p2('X') # do nothing
p2('X') # do nothing
p3() # +=1
p3() # +=1
p3() # +=1
# lookaround
for p in (h1, p1, p2, p3):
free_vars_in_scopes_of(p)
#free_vars_from_closure_of(p)
cell_content(p)
Output
1 X
2 X
3 X
h1 free vars in its scope ()
cells of h1: None
h2 free vars in its scope ()
cells of h2: None
h3 free vars in its scope ('c',)
cells of h3: 3
h4 free vars in its scope ('c', 'x')
cells of h4: 3,X
h1 and h2 are both not closures since they have no cell and no free-variables in their scope.
h3 and h3 are closures and share (in this case) the same cell and free-variable for c. h4 has a further free-variable x with its own cell.
Final considerations:
- the
__closure__ attribute and __code__.co_freevars can be used to check for values and names (identifiers) of the free-variables
- anti-analogies (in a broad sense) between
nonlocal and __code__.co_cellvars: nonlocal acts towards the outer function, __code__.co_cellvars instead towards the internal function