I talked time ago about some minor limitation (related to eval) of Python closures when compared to JavaScript ones. That's true, but the thing is that Python closures are particularly powerful in terms of introspection. In this previous post (and some older ones) I already talked about fn.__code__.co_cellvars, fn.__code__.co_freevars and fn.__closure__, as a reminder taken from here
- co_varnames — is a tuple containing the names of the local variables (starting with the argument names).
- co_cellvars — is a tuple containing the names of local variables that are referenced by nested functions.
- co_freevars — is a tuple containing the names of free variables; co_code is a string representing the sequence of bytecode instructions.
And the __closure__ attribute of a function object is a tuple containing the cells for the variables that it has trapped (the free variables).
# closure example (closing over wrapper and counter variables from the enclosing scope)
def create_formatter(wrapper: str) -> Callable[[str], str]:
counter = 0
def _format(st: str) -> str:
nonlocal counter
counter += 1
return f"{wrapper}st{wrapper}"
return _format
format = create_formatter("|")
print(format("a"))
# |a|
# the closure attribute is a tuple containing the trapped values
print(f"closure: {format.__closure__}")
print(f"freevars: {format.__code__.co_freevars}")
# closure: (cell at 0x731017299ea0: int object at 0x6351ad1bd1b0, cell at 0x731017299de0: str object at 0x6351ad1cd2e8)
# freevars: ('counter', 'wrapper')
A cell is a wrapper object pointing to a value, the trapped variable, it's an additional level of indirection that allows the closure to share the value with the enclosing function and with other closures that could also be trapping that value, so that if any of them changes the value, this is visible for all of them.
def create_formatters(format_st: str) -> Callable[[str], str]:
"""
creates two formatter closures that share the same 'format' free variable.
one of them can disable the formatting by setting the format string to an empty string.
"""
def _prepend(st: str) -> str:
nonlocal format_st
if st == "disable":
format_st = "" # Example of modifying the closed-over variable
return
return f"{format_st}{st}"
def _append(st: str) -> str:
return f"{st}{format_st}"
return _prepend, _append
prepend, append = create_formatters("!")
print(prepend("Hello"))
print(append("Hello"))
# !Hello
# Hello!
prepend("disable")
print(prepend("World")) # Output: World (since format_st was modified to "")
print(append("World")) # Output: World
# !Hello
# Hello!
Here you can find a perfect explanation of co_freevars, co_cellvars and closure cells:
Closure cells refer to values needed by the function but are taken from the surrounding scope.
When Python compiles a nested function, it notes any variables that it references but are only defined in a parent function (not globals) in the code objects for both the nested function and the parent scope. These are the co_freevars and co_cellvars attributes on the __code__ objects of these functions, respectively.
Then, when you actually create the nested function (which happens when the parent function is executed), those references are then used to attach a closure to the nested function.
A function closure holds a tuple of cells, one each for each free variable (named in co_freevars); cells are special references to local variables of a parent scope, that follow the values those local variables point to.
If we have a function factory that creates a closure, each time we invoke it we'll get a new function object with its __closure__ attribute pointing to its own object (a tuple), but with __code__ pointing to the same code object. So all those instances of the function have the same bytecodes and metainformation, but each instance has its own state (closure cells/freevars).
The closure "superpowers" that Python features are:
1) As we saw above, ee can easily check if a function is a closure (has cells/freevars) just by checking if its __closure__ attribute is not None (or if its __code__.co_freevars tuple is not empty).
2) We can see "from outside" the values of the closure freevars (the names, the values, and combine both with a simple "show_cell_values" function). And furthermore, we can modify them, just by modifying the contents of the cells in fn.__closure__. It's what we could call "closure introspection".
# combining the names in co_freevars and the values in closure cells to nicely see the trapped values
def show_cell_values(fn) -> dict[str, CellType]:
return {name: fn.__closure__[i].cell_contents
for i, name in enumerate(fn.__code__.co_freevars)
}
def cell_name_to_index_map(fn) -> dict[str, int]:
return {name: i for i, name in enumerate(fn.__code__.co_freevars)}
def get_freevar(fn, name: str) -> Any:
name_to_index = cell_name_to_index_map(fn)
return fn.__closure__[name_to_index[name]].cell_contents
def set_freevar(fn, name: str, value: Any) -> Any:
name_to_index = cell_name_to_index_map(fn)
fn.__closure__[name_to_index[name]].cell_contents = value
def create_formatter(wrapper: str) -> Callable[[str], str]:
counter = 0
def _format(st: str) -> str:
nonlocal counter
counter += 1
return f"{wrapper}st{wrapper}"
return _format
format = create_formatter("|")
print(f"format cells: {show_cell_values(format)}")
print(f"format 'wrapper' freevar before: {get_freevar(format, 'wrapper')}")
print(format("a"))
# format cells: {'counter': 1, 'wrapper': '|'}
# format 'wrapper' freevar before: |
# |st|
set_freevar(format, 'wrapper', '-')
print(f"format 'wrapper' freevar after: {get_freevar(format, 'wrapper')}")
print(format("a"))
# format 'wrapper' freevar after: -
# -st-
No comments:
Post a Comment