简体   繁体   中英

ParamSpec for a pre-defined function, without using generic Callable[P]

I want to write a wrapper function for a known function, like

    def wrapper(*args, **kwargs)
         foo()
         return known_function(*args, **kwargs)

How can i add type-annotations to wrapper , such that it exactly follows the type annotations of known_function


I have looked at ParamSpec , but it appears to only work when the wrapper-function is generic and takes the inner function as argument.

    P = ParamSpec("P")
    T = TypeVar('T')
    def wrapper(func_arg_that_i_dont_want: Callable[P,T], *args: P.args, **kwargs: P.kwargs)
         foo()
         return known_function(*args, **kwargs)

Can i force the P to only be valid for known_function , without linking it to a Callable -argument?

PEP 612 as well as the documentation of ParamSpec.args and ParamSpec.kwargs are pretty clear on this:


These “properties” can only be used as the annotated types for *args and **kwargs , accessed from a ParamSpec already in scope.

- Source: PEP 612 ("The components of a ParamSpec" -> "Valid use locations")


Both attributes require the annotated parameter to be in scope.

- Source: python.typing module documentation ( class typing.ParamSpec -> args / kwargs )


They [parameter specifications] are only valid when used in Concatenate , or as the first argument to Callable , or as parameters for user-defined Generics.

- Source: python.typing module documentation ( class typing.ParamSpec , second paragraph)


So no, you cannot use parameter specification args / kwargs , without binding it a concrete Callable in the scope you want to use them in.

I question why you would even want that. If you know that wrapper will always call known_function and you want it to (as you said) have the exact same arguments, then you just annotate it with the same arguments. Example:

def known_function(x: int, y: str) -> bool:
    return str(x) == y


def wrapper(x: int, y: str) -> bool:
    # other things...
    return known_function(x, y)

If you do want wrapper to accept additional arguments aside from those passed on to known_function , then you just include those as well:

def known_function(x: int, y: str) -> bool:
    return str(x) == y


def wrapper(a: float, x: int, y: str) -> bool:
    print(a ** 2)
    return known_function(x, y)

If your argument is that you don't want to repeat yourself because known_function has 42 distinct and complexly typed parameters, then (with all due respect) the design of known_function should be covered in copious amounts gasoline and set ablaze.


If you insist to dynamically associate the parameter specifications (or are curious about possible workarounds for academic reasons), the following is the best thing I can think of.

You write a protected decorator that is only intended to be used on known_function . (You could even raise an exception, if it is called with anything else to protect your own sanity.) You define your wrapper inside that decorator (and add any additional arguments, if you want any). Thus, you'll be able to annotate its *args / **kwargs with the ParamSpecArgs / ParamSpecKwargs of the decorated function. In this case you probably don't want to use functools.wraps because the function you receive out of that decorator is probably intended not to replace known_function , but stand alongside it.

Here is a full working example:

from collections.abc import Callable
from typing import Concatenate, ParamSpec, TypeVar


P = ParamSpec("P")
T = TypeVar("T")


def known_function(x: int, y: str) -> bool:
    """Does thing XY"""
    return str(x) == y


def _decorate(f: Callable[P, T]) -> Callable[Concatenate[float, P], T]:
    if f is not known_function:  # type: ignore[comparison-overlap]
        raise RuntimeError("This is an exclusive decorator.")

    def _wrapper(a: float, /, *args: P.args, **kwargs: P.kwargs) -> T:
        """Also does thing XY, but first does something else."""
        print(a ** 2)
        return f(*args, **kwargs)
    return _wrapper


wrapper = _decorate(known_function)


if __name__ == "__main__":
    print(known_function(1, "2"))
    print(wrapper(3.14, 10, "10"))

Output as expected:

False
9.8596
True

Adding reveal_type(wrapper) to the script and running mypy gives the following:

Revealed type is "def (builtins.float, x: builtins.int, y: builtins.str) -> builtins.bool"

PyCharm also gives the relevant suggestions regarding the function signature, which it infers from having known_function passed into _decorate .

But again, just to be clear, I don't think this is good design. If your "wrapper" is not generic, but instead always calls the same function, you should explicitly annotate it, so that its parameters correspond to that function. After all:

Explicit is better than implicit.

- Zen of Python , line 2

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM