-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Ability to use Callable type alias when annotating functions #1641
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
Comments
But I ask myself, how would we do that in Python 3 using inline function annotations (PEP 3107)? I'm not sure this is a desirable feature. |
In a decorator you may want to give an inner function (that is going to be returned by the decorator) a type that is a type variable (if mypy supported that, which it doesn't yet). That has the same problem with regards to inline function annotation syntax. (Or if we had variadic type variables for abstracting over arbitrary function argument lists (python/typing#193) the same would apply to some extent.) |
Would a decorator be suitable for this? Something along the lines of from typing import Callable, Optional, coerce
Validator = Callable[[Any], Optional[str]]
@coerce(Validator)
def check_string(val):
... # raise if ``val`` is invalid
return val
from typing import Callable, NamedTuple, coerce
import math
Constraint = Callable[[float], float]
class Clip(Constraint, NamedTuple):
lower: float
upper: float
def __call__(self, x: float) -> float:
return max(self.lower, min(self.upper, x))
@coerce(Constraint)
def softplus(x: float) -> float:
return math.log(1 + math.exp(x))
@coerce(Constraint)
def null(x: float) -> float:
return x Without |
I had this issue as well. I wanted to use the type as a interface: from typing import Optional, Callable, Tuple, TypeVar
T = TypeVar("T")
parser = Callable[[str], Optional[Tuple[str, T]]]
# cant use parser[str] as type for take_one
def take_one(s):
return s[1:], s[1]
def match(to_match: str) -> parser[str]:
# cant use parser[str] as type for p
def p(s: str) -> Optional[Tuple[str, str]]:
if s.startswith(to_match):
return s[len(to_match):], to_match
else:
return None
return p and have a function with signature |
I also had this issue - I have a bunch of super short handler that take a relatively large number of arguments, ideally had the argument typed as a single Tuple and I could alias the type to a variable. However, since tuple unpacking in function definitions is not supported in python 3, we have to move away from that system. This resulted in having to define each variable one one line since the type got too long, and thus some handlers with function definitions longer than their code. Being able to just alias the whole function type would solve that problem. An alternative would be being able to declare an argument type alias similar to how I was defining a single Tuple type before:
|
I did a cursory skim through the PEP but could not find this issue being addressed there. Could we comment the resolution here, as google anyway drops me here? |
(Sorry, what I posted was for a different issue, so I deleted it here. What PEP did you skim?) |
@gvanrossum I went through PEP 3107. |
What were you looking for? PEP 3107 doesn't mention Callable -- that was introduced in PEP 484, (which chronologically came after PEP 3107). |
Thank you for pointing me to PEP 484 :) and sorry for the delayed response. I wanted to make sure that I read the PEP in completion and not reply in haste. From what I understand from PEP 484, the suggestion in this issue is not supported. To be a little more explicit, here is example of the use case # in ./activations.py
ActivationFunction = Callable[[float], float]
def sigmoid(x):
# type: ActivationFunction
return 1 / (1 + math.exp(-x))
def relu(x):
# type: ActivationFunction
return x if x > 0. else 0.
#-----------------------------
# in ./loader.py
import activations
def load_activation(name: str) -> ActivationFunction:
return getattr(activations, name) Currently this can be achieved by class ActivationFunction(metaclass=abc.ABCMeta):
"""Base class for all activation functions.
.. note::
Please do not add any other method or store member fields unless absolutely
necessary. Keep this class extremely slim moving logic into pure functions to
aid testing.
"""
@abc.abstractmethod
def transform(x: float) -> float:
raise NotImplementedError
class Sigmoid(ActivationFunction):
def transform(x):
return 1 / (1 + math.exp(-x))
class Relu(ActivationFunction):
def transform(x):
return x if x > 0. else 0. Preferably, I would be able to use functions and use the typing system to check the function signature and the return type instead of creating classes. From at least what I gathered from the PEP-484, this is not supported use case. Please do correct me if I am wrong. |
Can you do this with variable annotations? ActivationFunction = Callable[[Float], Float]
sigmoid: ActivationFunction
def sigmoid(x):
return 1 / (1 + math.exp(-x)) |
That is correct. There is not even a finished design -- your proposal (use |
@gvanrossum I see I see :) # ./activation.py
from typing import Callable
import math
ActivationFunction = Callable[[float], float]
sigmoid: ActivationFunction
def sigmoid(x):
return 1 / (1 + math.exp(-x))
relu: ActivationFunction
def relu(x):
return x if x > 0. else 0. $ mypy activation.py
activation.py:10: error: Name 'sigmoid' already defined on line 9
activation.py:15: error: Name 'relu' already defined on line 14 |
For a language like python where everything is an object I argue that the syntax proposed by @PaperclipBadger is the expected behavior since
so why should this not work for functions (since they are also objects)? Separating the type information from the actual implementation is also a thing in functional languages like Haskell where one would define a simple function that adds two integers like so:
From that point of view something like the following looks quite natural.
In my opinion this is also a much nicer way of adding gradual typing to existing code bases compared to using stub files. |
As long as we don't mess with the standard PEP 484 way to define functions, I think that this is a reasonable proposal. Perhaps someone can come up with a draft implementation as a PR? Then we could put it in mypy_extensions and write a PEP about it. |
I found myself looking at a few related issues attempting to limit the scope of input for the built in Not sure if this is idiomatic, but it seems to work alright. from decimal import Decimal as _Decimal, Context
StrictDecimalValue = typing.TypeVar(
'StrictDecimalValue', str, _Decimal, "_StrictDecimal"
)
class _StrictDecimal(_Decimal):
def __new__(
cls, value: StrictDecimalValue,
context: typing.Optional[Context] = None
) -> _Decimal:
if not isinstance(value, (str, _Decimal)):
raise ValueError("StrictDecimal must receive a str or Decimal type for value")
if context is None:
context = Context(prec=16, rounding=decimal.ROUND_UP)
return _Decimal(value, context).normalize()
StrictDecimal = typing.TypeVar('StrictDecimal', _Decimal, _StrictDecimal, "Decimal")
Decimal = _StrictDecimal
ZERO = Decimal('0.0')
ONE = Decimal(1.0) # error If this proposal could consider this type of use-case, or if you know of a better way I can do this today, I'd appreciate it. |
I don't think that this is important enough for a non-standard mypy extension. If somebody wants to move this issue forward, discussion can continue on typing-sig@ or the typing issue tracker. |
If I have a type alias that is a Callable and a function that matches that type alias, it would be nice to annotate the function using just the type alias.
Example:
The text was updated successfully, but these errors were encountered: