简体   繁体   中英

Chained references in python type annotations

Say I have a function that takes a value and a arbitrary number of functions, let's call the function for chain_call.

Without types a simple naive implementation would be:

def chain_call(input_value, *args):
    for function in args:
        input_value = function(input_value)
    return input_value

As you imagine, input_value could be anything really but it's always the same as the first and only required argument of the first Callable in *args: List[Callable] .

From here and forward the Callable ´s first and only required argument is the same type as the previous items return-type.

So far I've managed to define a quite generic type but it's too loose.

def chain_call(input_value: Any, *args: List[Callable[Any], Any]) -> Any: ...

What I'd really like is something like

T = TypeVar('T')

def chain_call(input_value: T, *args: List[Callable[T, ...], tr]) -> tr: ...

Where T for Callable n+1 is tr of Callable n and the final return-type is tr of Callable n_max . I'm not sure how to express this with the type system and would love any guidance.

Unfortunately, this is currently not something that's possible to type using PEP 484 type hints.

The best you can do is use overloads to approximate a signature: basically, we hard-code what the signature ought to be up to a certain number then fall back to inferring 'Any':

from typing import TypeVar, overload, Any, Callable

T1 = TypeVar('T1')
T2 = TypeVar('T2')
T3 = TypeVar('T3')
T4 = TypeVar('T4')

@overload
def chain_call(input_value: T1, 
               *f_rest: Callable[[T1], T1]) -> T1: ...
@overload
def chain_call(input_value: T1, 
               f1: Callable[[T1], T2],
               f2: Callable[[T2], T3],
               f3: Callable[[T3], T4],
               f4: Callable[[T4], Any],
               *f_rest: Callable[[Any], Any]) -> Any: ...
@overload
def chain_call(input_value: T1, 
               f1: Callable[[T1], T2],
               f2: Callable[[T2], T3],
               f3: Callable[[T3], T4]) -> T4: ...
@overload
def chain_call(input_value: T1, 
               f1: Callable[[T1], T2],
               f2: Callable[[T2], T3]) -> T3: ...
@overload
def chain_call(input_value: T1, 
               f1: Callable[[T1], T2]) -> T2: ...
def chain_call(input_value, *f_rest):
    for function in f_rest:
        input_value = function(input_value)
    return input_value

Here, I hard-coded what ought to happen up to 3 input functions (and started with an overload for the special case where all the callables happen to have the same input and output type).

This technique is how typeshed currently types things like the zip function, which can accept an arbitrary number of iterables.

Note: you may need to use the latest version of mypy from master for this code to work verbatim.

This fully typed function exists in dry-python/returns .

We call it flow :

from returns.pipeline import flow

assert flow('1', int, float, str) == '1.0'

The thing is that flow is fully typed via a custom mypy plugin we ship with our library. So, it will catch this error case (and many others ):

from returns.pipeline import flow

def convert(arg: str) -> float:
    ...

flow('1', int, convert)
# error: Argument 1 to "convert" has incompatible type "int"; expected "str"

Docs: https://returns.readthedocs.io/en/latest/pages/pipeline.html

Source: https://github.com/dry-python/returns/blob/0f7d02d0c491a7c65c74e6c0645f12fccc53fe18/returns/_internal/pipeline/flow.py

Plugin: https://github.com/dry-python/returns/blob/0f7d02d0c491a7c65c74e6c0645f12fccc53fe18/returns/contrib/mypy/_features/flow.py

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