I previously explored the unusual way Python handles default arguments/parameters in [this blog post](a post) (I’m still unsure whether to call them “arguments” or “parameters”!). This behavior stems from default parameters being bound when the function is defined, not when it’s called (i.e., late binding). My post explained how JavaScript, Kotlin, and Ruby differ by evaluating default parameter values during each function call. At the time, I hadn’t grasped the potential of this feature. However, after examining [the documentation](MDN documentation), I discovered that parameters can use previous parameters in its definition:
[```
function greet(name, greeting, message = `${greeting} ${name}`) { return [name, greeting, message]; }
| |
fun read( b: ByteArray, off: Int = 0, len: Int = b.size, ) { /*...*/ }
| |
`from dataclasses import dataclass from typing import Callable, Any import functools import inspect
@dataclass class LateBound: resolver: Callable
def _invoke_late_bound(callable: Callable, arg_name_to_value: dict[str, Any]) -> Any: """ invokes a callable passing over to it the parameters defined in its signature we obtain those values from the arg_name_to_value dictionary """ expected_params = inspect.signature(callable).parameters.keys() kwargs = {name: arg_name_to_value[name] for name in expected_params } return callable(**kwargs)
def _add_late_bounds(arg_name_to_value: dict[str, Any], late_bounds: list[str, Callable]): “““resolves late-bound values and adds them to the arg_name_to_value dictionary””” for name, callable in late_bounds: val = _invoke_late_bound(callable, arg_name_to_value) #this way one late bound can depend on a previous late boud arg_name_to_value[name] = val
def _resolve_args(target_fn: Callable, *args, **kwargs) -> dict[str, Any]: “““returns a dictionary with the name and value all the parameters (the ones already provided, the calculated latebounds and the normal defaults)””” # dictionary of the arguments and values received by the function at runtime # we use it to be able to calculate late_bound values based on other parameters arg_name_to_value: dict[str, Any] = {} arg_names = list(inspect.signature(target_fn).parameters.keys()) for index, arg in enumerate(args): arg_name_to_value[arg_names[index]] = arg arg_name_to_value = {**arg_name_to_value, **kwargs}
# obtain the values for all default parameters that have not been provided
# we obtain them all here so that late_bounds can depend on other (compile-time or late-bound) default parameters
#late bounds to calculate (were not provided in args-kwargs)
not_late_bounds = {name: param.default
for name, param in inspect.signature(target_fn).parameters.items()
if not isinstance(param.default, LateBound) and not name in arg_name_to_value
}
arg_name_to_value = {**arg_name_to_value, **not_late_bounds}
list rather than dictionary as order matters (so that a late-bound can depend on a previous late-bound)
late_bounds = [(name, param.default.resolver)
for name, param in inspect.signature(target_fn).parameters.items()
if isinstance(param.default, LateBound) and not name in arg_name_to_value
]
_add_late_bounds(arg_name_to_value, late_bounds) return arg_name_to_value
#decorator function def late_bind(target_fn: Callable | type) -> Callable | type: “““decorates a function enabling late-binding of default parameters for it””” @functools.wraps(target_fn) def wrapper(*args, **kwargs): kwargs = _resolve_args(target_fn, *args, **kwargs) return target_fn(**kwargs)
return wrapper`
| |
`from datetime import datetime from dataclasses import dataclass from late_bound_default_args import late_bind, LateBound
@late_bind def say_hi(source: str, target: str, greet: str, extra = LateBound(lambda: f"[{datetime.now():%Y-%m-%d_%H%M%S}]"), ): """""" return f"{greet} from {source} to {target}. {extra}"
@late_bind def say_hi2(source: str, target: str, greet: str, extra = LateBound(lambda greet: f"[{greet.upper()}!]"), ): """""" return f"{greet} from {source} to {target}. {extra}"
print(say_hi(“Xuan”, “Francois”, “Bonjour”)) print(say_hi2(“Xuan”, “Francois”, “Bonjour”))
#Bonjour from Xuan to Francois. [2024-02-02_002939] #Bonjour from Xuan to Francois. [BONJOUR!]
access to the “self” parameter in a late-bound method works also fine
@dataclass class Person: name: str birth_place: str
@late_bind def travel(self, by: str, start_city: str = LateBound(lambda self: self.birth_place), to: str = “Paris” ): """ """ return(f"{self.name} is travelling from {start_city} to {to} by {by}")
p1 = Person(“Xuan”, “Xixon”) print(p1.travel(“train”))
Xuan is travelling from Xixon to Paris by train`
| |