-
Notifications
You must be signed in to change notification settings - Fork 247
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make Callable more flexible #239
Comments
I could add another option (actually rather a variant of the second option) of allowing a signature object to be used, as here: from inspect import signature
def func(x: int, *, opt: str = 'full') -> int:
...
sign = signature(func)
def another_func(callback: Callable[sign]) -> None:
... In this way, a user could either use the above form (pointing to existing function as a template) or construct an arbitrary signature in a programmatic way: from inspect import Signature, Parameter
sign = Signature([Parameter('x', Parameter.POSITIONAL_OR_KEYWORD, annotation=int),
Parameter('opt', Parameter.KEYWORD_ONLY, default='full', annotation=str)],
return_annotation=int)
MyCallbackType = Callable[sign] |
What's the advantage of using Signature() over a template function? It
would require mypy to learn a lot of stuff about the Signature and
Parameter API, and it would probably encourage people to mistakenly think
that mypy understands dynamically constructed signatures (it would only
understand your example because all the arguments are compile-time
constants). Requiring an actual function leverages mypy's existing
understanding of function definitions, and makes it clear(er) that the
template must be statically understood.
I don't think the worry that people might accidentally try to call the
template should be a big concern.
|
There are a few additional things to consider (the latter 2 only apply to @gvanrossum 's approach) that we've discussed offline:
|
(1) the double underscore sounds fine. |
I agree that allowing If only templates are going to be supported I don't think that a template should be wrapped in some kind of special def template(__x: int, *, option: str='verbose') -> int:
raise NotImplementedError()
def func(callback: Callable[template]):
... The remaining question is does |
@gvanrossum I have noticed here Indeed, it is quite common for decorators to preserve all the variables (independently of their types) and only change the type of the return value of a function. In the mentioned comment, it was proposed to allow yet another use of It looks like there is an agreement on the initial issue (allowing single argument use for
|
I think I have a possible solution to this problem. It draws on the notion of a variadic generic we've been discussing over here: #193 (comment) (link is to the closest-to-complete proposal as a comment, but the entire thread is worth reading, if you're getting there from this thread). Some of what I'm saying here has been already stated in this thread; I may be restating some of it to lay out a proposal in a way I hope is understandable. There are three problems we need to solve, if we want to be able to describe higher-order functions in enough generality to be able to specify the type of most decorators:
1. Specifying complete argumentsTo talk about functions with full accuracy in our type system, we need our type system to be able to accurately spell their signatures. The design space here has a number of degrees of freedom, but here is one possibility, described by example: # type: Callable[[Arg('a')[int], Arg('b')[int]], int]
def add(a: int, b: int) -> int:
return a + b
# type: Callable[[Arg('a')[int], StarArg('more_ints')[int]], int]
def add(a: int, *more_ints: int) -> int:
sum = a
for i in more_ints:
sum += i
return sum
# type: Callable[[Arg('a')[str], StarArg('args')[str], KeywordArg('kwargs')[str]], str]
def sprintf(fmt: str, *args: str, **kwargs: str) -> str: ...
T1 = TypeVar('T1')
T2 = TypeVar('T2')
# type: Callable[[Arg('a')[T1], Arg('b')[T2]], Tuple[T1, T2]]
def two_things(a: T1, b: T2) -> Tuple[T1, T2]:
return (a, b)
T = TypeVar('T')
# type: Callable[[Arg('a')[T], Arg('b')[T], OptArg('c')[T]], Tuple[T, ...]]
def two_or_three_things(a: T, b: T, c: T = None) -> Tuple[T, ...]:
if c is not None:
return (a, b, c)
return (a, b) The simple way (without arg names and such) of specifying a Note that for types 2. Capturing arguments for use in higher-order functionsWe're used to using type variables to capture types in our function signatures, and show how the various argument and return types of a function relate to each other: R = TypeVar('R')
T1 = TypeVar('T1')
T2 = TypeVar('T2')
# Yes, I know this example is silly
def switch_args(f: Callable[[T1, T2], R]) -> Callable[[T2, T1], R]:
def ret(a, b):
return f(b, a)
return ret But for higher-order functions, that's not quite enough! We need to be able to capture the argument kinds and names, too. def cheer(name: str, punctuation: str) -> str:
return "Hooray for %s%s" % (name, punctuation)
switch_cheer = switch_args(cheer)
switch_cheer("!!!", "Sally") # "Hooray for Sally!!!"
switch_cheer(name=Joseph, punctuation="?") # THIS IS AN ERROR Let's have a new idea, similar to a R = TypeVar('R')
A1 = ArgVar('A1')
A2 = ArgVar('A2')
def switch_args(f: Callable[[A1, A2], R]) -> Callable[[A2, A1], R]:
... # The implementation of this particular example preserving names is quite tricky.
# It is possible, but this margin blah blah you know the drill. It involves the inner
# function using `*args` and `**kwargs`, and I don't think there's an easier way
def cheer(name: str, punctuation: str) -> str:
return "Hooray for %s%s" % (name, punctuation)
switch_cheer = switch_args(cheer)
switch_cheer("!!!", "Sally") # "Hooray for Sally!!!"
switch_cheer(name=Joseph, punctuation="?") # "Hooray for Joseph?" 3. Sequences of arguments.Steps 1. and 2. are still not enough. We need to talk about arbitrary sequences of arguments. That's where the whole variadic thing comes in, as presaged above in the link to the other thread (running out of time at the computer here, so this section will be a little sketchier for now). As = ArgVar('As', variadic=True)
def cast_to_int_decorator(f: Callable[[As, ...], SupportsInt]) -> Callable[[As, ...], int]:
def ret(*args, **kwargs):
return int(f(*args, **kwargs))
return ret Commentary
I have to run now, but I'm happy to expand on any part of this that's unclear when I get back to a computer. |
@sixolet Thanks! Your idea looks interesting, but the proposal is quite complicated indeed. However, I could not propose anything simpler. I don't think that we need to support all the possible situations/use cases, but rather have a simple proposal that covers some typical situations with decorators (like capturing all arguments completely). I am not yet sure what part of this we need. There are two questions that could be discussed at this point:
|
Cool! I hadn't thought of putting If we can put a type var into an arg var, there's no reason not to put a specific known type in there too, for when you want to capture name/kind info, but you know the type -- you could have While considering your comment this morning, I thought for a moment you'd narrowed down the two-and-a-half new concepts in my post to two, because of the conceit that if you put a variadic One thing that I don't like about putting type variables into argument variables is that it "hides" the type vars in the actual signature of the callable -- ideally you'd want them all visible. Let's say Here's an idea that fixes the hidden-type-variable problem: What if T = TypeVar('T')
A = ArgVar('A')
A_T = A[T] And then |
I'd previously thought that |
As for simplicity: I think the vast majority of use cases want "please just capture all the arguments of this function". To that end, I think the Ts = TypeVar('Ts', variadic=True)
A = ArgVar('A')
AllArgs = Expand[A[Ts]] This uses the last form of That way you can write the majority of decorator cases only having the vague idea that from typing import AllArgs, TypeVar
R = TypeVar('R')
def add_initial_int_arg(f: Callable[[AllArgs], R]) -> Callable[[int, AllArgs], R]:
... |
from typing import OtherArgs, TypeVar, Callable
R = TypeVar('R')
def change_ret_type(f: Callable[[OtherArgs], R]) -> Callable[[OtherArgs], int]: ...
def add_initial_int_arg(f: Callable[[OtherArgs], R]) -> Callable[[int, OtherArgs], R]: ...
def fix_initial_str_arg(f: Callable[[str, OtherArgs], R]) -> Callable[[OtherArgs], int]:
def ret(*args, **kwargs):
f('Hello', *args, **kwargs)
return ret It looks like your proposal also covers the last example, maybe this is exactly what we need? @sixolet what do you think? |
Yeah The places you need the more powerful |
Here's a challenge: can we formalize the meaning of |
I think that some of the less common cases of decorators could be typed with function templates discussed earlier in this thread #239 (comment) I think it could be possible to formalize For example, a short definition: Ts = TypeVar('Ts', variadic=True)
OtherArgs = Expand[Ts] with an exception that expanded type variables @gvanrossum what do you think about this idea? |
Hmm. Personally I find explicitly spelling name and kind in a signature (a la Unless they are compatible and I am failing to see how. |
The other thing to do if we do go the way of template functions is to be very careful about documentation -- "template" is a word that means something quite like "generic" in C++ land, and we run a risk of confusing people. |
@sixolet I agree that the name "template function" could be changed to avoid confusions, maybe "stub function"? In fact, stub functions are much less verbose than full "systematic" signatures (see discussion above, and this is a relatively simple function, real life functions will have verbose signatures even with your shorter syntax). Moreover, people could already have functions to use as stub functions in their code. I think there is already certain agreement on "stub functions". Still, the |
Made a self-contained proposal for only the part about specifying argument names and kinds here: #264 |
This is the result of the discussion in python/typing#239 It allows you to specify Callable objects that have optional, keyword, keyword-only, and star (and double-star!) arguments. This particular commit is a first draft.
…thing you can do (#2607) Implements an experimental feature to allow Callable to have any kind of signature an actual function definition does. This should enable better typing of callbacks &c. Initial discussion: python/typing#239 Proposal, v. similar to this impl: python/typing#264 Relevant typeshed PR: python/typeshed#793
How does this issue differ from #264? |
This issue is a parallel discussion more focused on template-based syntax and variadic-type-variable-like approach ( |
OK, sorry if I misread this issue comments, but how do I annotate these functions in one annotation: def f0(x: int) -> int:
pass
def f1(x: int, y: int = 0) -> int:
pass ? I want something like: Callable[[int, [int]], int] |
You can now use a callable protocol (https://mypy.readthedocs.io/en/latest/protocols.html#callback-protocols). |
@JelleZijlstra, thank you! That's what I needed. |
…thing you can do (#2607) Implements an experimental feature to allow Callable to have any kind of signature an actual function definition does. This should enable better typing of callbacks &c. Initial discussion: python/typing#239 Proposal, v. similar to this impl: python/typing#264 Relevant typeshed PR: python/typeshed#793
We keep hearing requests for a way to declare a callback with either specific keyword arguments or just optional arguments. It would be nice if we could come up with a way to describe these.
Some suggestions:
The text was updated successfully, but these errors were encountered: