Indeed, that complicates and necessitates discerning between
- unary (
(T)->T) functions (letās call them type EndoFunction[T] = Callable[[T], T])
- and n-ary (
(*Ts)->tuple[*Ts]) functions (type TupleFunction[*Ts] = Callable[[*Ts], tuple[*Ts]])
not by type, but rather by inspection of the signature of the function thatās to be raised to a power, like:
def is_unary_function[**PS, RT](function: Callable[PS, RT]) -> bool:
function_params = inspect.signature(function).parameters.values()
param_counts = collections.Counter(param.kind for param in function_params)
pos_params_count = (param_counts[inspect.Parameter.POSITIONAL_ONLY]
+ param_counts[inspect.Parameter.POSITIONAL_OR_KEYWORD])
return pos_params_count == 1
With that is_unary_function helper and the defined types, we can use below function-to-the-power func_pow function (with some casts to help mypy understand):
@overload
def func_pow[T](function: EndoFunction[T], power: int) -> EndoFunction[T]: ...
@overload
def func_pow[*Ts](function: TupleFunction[*Ts], power: int) -> TupleFunction[*Ts]: ...
def func_pow[T, *Ts](function: EndoFunction[T] | TupleFunction[*Ts], power: int) -> EndoFunction[T] | TupleFunction[*Ts]:
if is_unary_function(function):
@functools.wraps(function)
def wrapper(arg: T, /) -> T:
for i in range(power):
arg = cast(EndoFunction[T], function)(arg)
return arg
else:
@functools.wraps(function)
def wrapper(*args: *Ts) -> tuple[*Ts]:
for i in range(power):
args = cast(TupleFunction[*Ts], function)(*args)
return args
if power < 0:
raise ValueError
return wrapper
This version of func_pow tests and type-checks okay for each of the aforementioned examples:
def square(x: int) -> int:
return x ** 2
assert func_pow(square, 2)(3) == (3 ** 2) ** 2 # squared twice
def pair_result(x: int, y: int) -> tuple[int, int]:
return divmod(x, y)
assert func_pow(pair_result, 2)(28, 5) == (1, 2) # not necessarily sane, but correct (28,5)->(5,3)->(1,2)
def double(x: int | tuple[int, ...]) -> int | tuple[int, ...]:
return 2 * x
assert func_pow(double, 0)(3) == 3 # doubling zero times, i.e. identify function
assert func_pow(double, 2)(3) == 12 # doubling an int twice
assert func_pow(double, 0)((2, 3)) == (2, 3) # doubling zero times, i.e. identify function
assert func_pow(double, 2)((2, 3)) == (2, 3, 2, 3, 2, 3, 2, 3) # doubling a tuple twice
That could be the operational basis for a decorator that enhances a decorated function from straight FunctionType to a Callable subtype that supports @ and ** (and perhaps currying).