【问题标题】:How to dynamically use Python's type hinting to indicate a return value to be the same as a parameter's type如何动态使用 Python 的类型提示来指示返回值与参数的类型相同
【发布时间】:2020-05-26 06:27:36
【问题描述】:

我正在使用 Python 从磁盘读取 JSON,并且正在尝试确保我的类型提示在下游是正确的。例如,像这样:

from typing import List

def verify_contains_ints(items: List[object]) -> List[int]:
    for item in items:
        if not isinstance(item, int):
            raise TypeError("List contents must be all ints")
    return items

我遇到的问题是我不想为 int、bool、str 等编写单独的函数。有没有办法动态指定我要验证的类型?我希望喜欢写的是这样的:

from typing import List

def verify_contains_type(items: List[object], inner_type = object) -> List[inner_type]:
    for item in items:
        if not isinstance(item, inner_type):
            raise TypeError(f"List contents must be all {inner_type}s")
    return items

在当前的类型提示状态下有没有办法做到这一点?

注意:这是我实际尝试做的简化版本。默认的 inner_type 在这里可能看起来很傻,但它对我的用例很重要。

【问题讨论】:

  • 这还不是一个答案,但是在 python 3.9 之后的某个时间应该可以对参数化泛型进行更好的动态类型检查:python.org/dev/peps/pep-0585

标签: python type-hinting typing


【解决方案1】:

我相信你可以在这里使用typing.cast,这有点难看。请注意,它没有运行时影响,它只是返回传递给它的内容,尽管它确实会产生函数调用开销。但它告诉类型检查器“这是现在的这种类型”。您应该使用TypeVar 使其通用,然后像您尝试做的那样简单地传递类型,并用Type 注释它

from typing import List, TypeVar, Type, cast

T = TypeVar('T')
def verify_contains_type(items: List[object], inner_type: Type[T]) -> List[T]:
    for item in items:
        if not isinstance(item, inner_type):
            raise TypeError("List contents must be all ints")
    return cast(List[T], items)


mylist: List[object] = [1, 2, 3, 4]

safe_mylist: List[int] = verify_contains_type(mylist, int)
print(safe_mylist[0] + safe_mylist[1])

mypy 现在很开心:

(py38) juan$ mypy --version
mypy 0.750
(py38) juan$ mypy test_typing.py
Success: no issues found in 1 source file

【讨论】:

  • 这看起来很好,除了当我尝试默认 inner_type 来反对人们不知道/关心类型时,mypy 抱怨。有什么想法吗?这是完美的,除了一个问题。
  • 如果您知道如何将 inner_type 默认设置为 object,我会将其设为可接受的答案 :)
  • @micseydel,请参阅我对默认类型 inner_type 的回答。如果这两个一起回答您的问题,请接受 juanpa.arrivillaga 的回答,而不是我的回答。
【解决方案2】:

是的,但前提是您可以构建一个新列表:

from typing import List, Type, TypeVar

T = TypeVar('T')

def verify_contains(items: List[object], inner_type: Type[T]) -> List[T]:
    # Mypy currently needs this hint to infer what 'clean_items' is
    # supposed to contain. Other type checkers may not.
    clean_items: List[T] = []
    for item in items:
        if not isinstance(item, inner_type):
            raise TypeError("List contents must be all ints")
        clean_items.append(item)
    return clean_items

如果您不熟悉什么是 TypeVar,它们是一种让您编写通用代码的方法。详情请见https://mypy.readthedocs.io/en/stable/generics.html

Type 让你指定你想要一个类对象,而不是一个类的实例。详情请见https://www.python.org/dev/peps/pep-0484/#the-type-of-class-objects


我们需要创建一个新列表的原因是,如果我们不这样做,您可能会由于突变而在您的代码中引入一个错误。例如:

original: List[object] = [3, 2, 1]
all_ints = verify_contains(original, int)

# Legal, since strs are a kind of object
original.append("boom")

# ...but if verify_contains doesn't return a copy, this will
# print out [3, 2, 1, "boom"]!
print(all_ints)

如果您可以忽略此潜在错误,请按照其他一些答案的建议使用强制转换。


另一种替代方法可能是只使用像 Pydantic 这样的库,而不是自己编写此验证逻辑。

这是我个人会采用的方法:我可以专注于使用 PEP 484 类型编写高级模式,并让库为我处理验证。

【讨论】:

    【解决方案3】:

    添加到the answer of juanpa.arrivillaga,但支持默认inner_type object。我能找到的最好方法是结合使用typing.Uniontyping.overload。我必须承认这很冗长,但至少不需要对代码进行功能更改。

    解决方案

    from typing import List, Type, TypeVar, Union, cast, overload, 
    
    T = TypeVar('T')
    
    
    @overload
    def verify_contains_type(items: List[object], inner_type: Type[T]) -> List[T]:
        ...
    
    @overload
    def verify_contains_type(
        items: List[object], inner_type: Type[object] = object
    ) -> List[object]:
        ...
    
    def verify_contains_type(
        items: List[object], inner_type: Union[Type[T], Type[object]] = object
    ) -> Union[List[T], List[object]]:
        for item in items:
            if not isinstance(item, inner_type):
                raise TypeError(f"List contents must be all {inner_type!r}")
        return cast(List[T], items)
    

    之后:

    mylist: List[object] = [1, 2, 3, 4]
    
    # Revealed type is 'builtins.list[builtins.float*]'
    my_floats = verify_contains_type(mylist, float)
    
    
    # Revealed type is 'builtins.list[builtins.object]'
    my_whatevers = verify_contains_type(mylist)
    

    说明

    在分析函数的用法时,类型检查器只会查看@overload 函数定义,按照指定的顺序检查它们,直到找到匹配项。实际函数中的类型注解考虑在内。

    在分析函数本身内部的代码时,类型检查只会使用实际函数的类型注解,而忽略@overload的定义。

    关于@overload的更多信息:

    【讨论】:

    • 我还没有花时间完全理解这一点(尤其是超载部分),但Union[List[T], List[object]] 不会破坏我在这里尝试做的事情的目的吗?
    • @micseydel,不,Union 不会被类型检查器使用,但我理解这种混淆。我添加了一些额外的解释,并且可以推荐 Mypy 文档以更深入地了解一般的函数重载。
    猜你喜欢
    • 1970-01-01
    • 2018-02-23
    • 2013-10-13
    • 2020-02-23
    • 2016-02-29
    • 2016-12-23
    • 1970-01-01
    • 2011-02-02
    • 2018-12-24
    相关资源
    最近更新 更多