【问题标题】:How to use typing hints with an optional first parameter如何使用带有可选第一个参数的输入提示
【发布时间】:2021-10-15 09:12:33
【问题描述】:

简介

我有一个名为 fun(start, stop, divisors)fun(stop, divisors) 的函数。

  1. 我想按这个特定的顺序调用参数。
  2. 我想以一种不会出现任何类型提示错误的方式实现它。

如果我放宽这两个限制中的任何一个,实施起来就很容易。例如,放宽对订单的限制可以这样做:

def fun(stop: int, divisors: List[int], start: int=0) -> int:
    ...
    return 0

fun(5, [1, 2, 3])
fun(5, [1, 2, 3], start=2)

或对类型提示的缓和显示在下面的文件中,但参数的顺序正确。

问题

我应该如何编写我的代码,以便我的函数参数是:

  1. 静态类型;
  2. 按特定顺序:(start[optional], stop, divisors);
  3. 给出 0 个 mypy 错误?

尝试

import typing
from typing import Union, Optional

Start = int
Stop = int
Divisor = int
Divisors = list[Divisor]


@typing.overload
def fun1(x: Start, y: Stop, divisors: Divisors) -> int:
    ...


@typing.overload
def fun1(y: Stop, divisors: Divisors) -> int:
    ...


def fun1(*args) -> int:
    if (arglen := len(args)) not in [2, 3]:
        raise TypeError("Function expected 2 or 3 arguments, got", arglen)
    if arglen == 2:
        args = [0] + list(args)
    start, stop, divisors = args
    return 0


def fun2(x, y, divisors) -> int:
    if divisors is None:
        start, stop, divisors = 0, x, y
    else:
        start, stop = x, y
    return 0


def fun3(*args) -> int:
    if (arglen := len(args)) == 3:
        start: Stop = args[0]
        stop: int = args[1]
        divisors: Divisors = args[2]
    elif arglen == 2:
        start: Stop = 0
        stop: int = args[0]
        divisors: Divisors = args[1]
    else:
        raise TypeError(
            f"Too {'few' if arglen == 0 else 'many'} values to unpack (2-3), got",
            arglen,
        )
    return 0


def fun4(*args) -> int:
    if (arglen := len(args)) == 3:
        pass
    elif arglen == 2:
        args = [0] + list(args)
    else:
        raise TypeError(
            f"Too {'few' if arglen == 0 else 'many'} values to unpack (2-3), got",
            arglen,
        )
    start: Stop = args[0]
    stop: int = args[1]
    divisors: Divisors = args[2]
    return 0

Output from mypy

typing_test_PE_001.py:20: error: Overloaded function implementation does not accept all possible arguments of signature 1
typing_test_PE_001.py:20: error: Overloaded function implementation does not accept all possible arguments of signature 2
typing_test_PE_001.py:24: error: Incompatible types in assignment (expression has type "List[int]", variable has type "Tuple[Any, ...]")
typing_test_PE_001.py:43: error: Name "start" already defined on line 39
typing_test_PE_001.py:44: error: Name "stop" already defined on line 40
typing_test_PE_001.py:45: error: Name "divisors" already defined on line 41
typing_test_PE_001.py:58: error: Incompatible types in assignment (expression has type "List[int]", variable has type "Tuple[Any, ...]")
Found 7 errors in 1 file (checked 1 source file)

【问题讨论】:

  • def fun(arg1: int, arg2: Union[int, List[int]], arg3: Optional[int] = None)?
  • 只要你用 Python 编写代码,你就永远不会得到静态类型的函数参数。您的 easy implementation 示例比您可能得到的任何答案都更具可读性和 Pythonic。
  • @Woodford 你可以在 Python 中使用类型提示和像 mypy 这样的类型检查器进行静态类型检查
  • @juanpa.arrivillaga 是的,但它不会在运行时强制执行。
  • @Woodford 嗯?静态类型检查不应该在运行时强制执行。

标签: python python-3.x type-hinting mypy python-typing


【解决方案1】:

让我们看看slice is hinted

class slice(object):
    start: Any
    step: Any
    stop: Any
    @overload
    def __init__(self, stop: Any) -> None: ...
    @overload
    def __init__(self, start: Any, stop: Any, step: Any = ...) -> None: ...
    __hash__: None  # type: ignore
    def indices(self, len: SupportsIndex) -> Tuple[int, int, int]: ...

range

class range(Sequence[int]):
    start: int
    stop: int
    step: int
    @overload
    def __init__(self, stop: SupportsIndex) -> None: ...
    @overload
    def __init__(self, start: SupportsIndex, stop: SupportsIndex, step: SupportsIndex = ...) -> None: ...
    [...]

基本上,您分别提示两个重载版本,一个带有可选的第一个参数,一个没有。 (所以基本上,你的尝试 #2。)

from typing import overload, List

@overload
def fun(start: int, stop: int, divisors: List[int]):
    ...

@overload
def fun(stop: int, divisors: List[int]):
    ...

def fun(start, stop, divisors=None):
    if divisors is None:
        divisors = stop
        stop = start
        start = 0

    ...

fun(1, 2, [1,2,3])  # OK
fun(2, [1,2,3])  # OK

如果你愿意,你也可以让这两个变体只接受位置参数:

# e.g.
def fun(start, stop, divisors=None, /):
    ...

【讨论】:

  • 在一般情况下,您可能必须在 fun 中使用强制转换才能使 mypy 满意,也许不是在这里,我没有检查过但是......
  • 我认为这里的关键问题是mypy 抱怨,因为def fun(*args) 无法接受重载......只是为了特别指出 OP 的问题。
  • 我试图找到一个使用重载的解决方案,但找不到一个 MyPy 可以接受的应用 --strict 设置(由于缺少具体实现的类型提示,使用 --strict 失败)
  • 啊,如果我们让两个变体都只接受位置参数,这会自行解决。见github.com/python/mypy/issues/6187。似乎要么必须使参数重载匿名/仅位置(使用__callable_1 等),要么将 **kwargs 添加到实现中。
  • 嗯,我不太了解其中的复杂性,无法判断这是错误、权衡还是必需品。
猜你喜欢
  • 2019-02-06
  • 1970-01-01
  • 2014-12-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-03-09
相关资源
最近更新 更多