如何使用预定义类型定义函数的类型(参数和返回类型)?

9
我可以为一个预定义的类型定义一个函数签名(包括参数和返回类型)。
假设有这样一个类型:
safeSyntaxReadType = Callable[[tk.Tk, Notebook, str], Optional[dict]]

这意味着 safeSyntaxReadType 是一个接收 3 个参数(如上所列类型)的函数,它可以返回一个字典,也可能不返回任何东西。

现在假设我使用了一个名为safeReadJsonFile的函数,其签名为:

def safeReadJsonFile(root = None, notebook = None, path = ''):

我想在函数签名中为函数 safeReadJsonFile 分配类型safeSyntaxReadType,可能类似于:

def safeReadJsonFile:safeSyntaxReadType(root = None, notebook = None, path = ''):

但是这种语法是不可行的。类型分配的正确语法是什么?
我可以这样做:
def safeReadJsonFile(root:tk.Tk = None, notebook:Notebook = None, path:str = '') -> Optional[dict]:

但我希望避免这种情况。

阅读了很多(所有的类型文档,以及一些PEP544),我发现没有这样的语法能够在定义时轻松地为整个函数分配一个类型(最接近的是@typing.overload,但并不完全符合我们的需求)。

但作为可能的解决方法,我实现了一个装饰函数,可以帮助轻松地分配类型:

def func_type(function_type):
    def decorator(function):
        def typed_function(*args, **kwargs):
            return function(*args, **kwargs)
        typed_function: function_type  # type assign
        return typed_function
    return decorator

使用方法如下:

greet_person_type = Callable[[str, int], str]

def greet_person(name, age):
    return "Hello, " + name + " !\nYou're " + str(age) + " years old!"

greet_person = func_type(greet_person_type)(greet_person)
greet_person(10, 10) # WHALA! typeerror as expected in `name`: Expected type 'str', got 'int' instead

现在我需要帮助:由于某些原因,类型检查器(PyCharm)不会为使用装饰语法的代码提供类型提示,尽管这种语法本应该是等价的:

@func_type(greet_person_type)
def greet_person(name, age):
    return "Hello, " + name + " !\nYou're " + str(age) + " years old!"

greet_person(10, 10)  # no type error. why?

我认为装饰器无法正常工作是因为装饰并没有改变原始的greet_person函数,所以从返回的装饰函数中得到的类型信息对于使用原始的greet_person函数时并没有影响。

我该如何使装饰器方法正常工作呢?


2
我认为在定义函数时你不能这样做。但是,如果你将一个函数传递给另一个函数作为参数,你可以使用你的新类型作为提示。此外,“dict or None”是否正确?我一直使用“Optional[dict]”,但也许“or”是一种更新的语法? - Dan
我不是Python类型的专家。但在TypeScript中,这是您为函数分配类型的方法。对于“dict或None”事物,在PyCharm中这对我有效,因此对我来说是可以接受的。 - Eliav Louski
对于“字典或None”的问题,经过重新检查,我发现如果在函数签名中定义它,则可以工作,但如果在Callable中使用它,则会返回Any...所以Optional[dict]可能是正确的语法。 - Eliav Louski
@EliavLouski 你可以使用Mypy包来实现这个。 - Siva Sankar
1个回答

8

只需将该函数分配给表示特定可调用类型的名称即可。

Greetable = Callable[[str, int], str]

def any_greet_person(name, age):
    ...

typed_greet_person: Greetable = any_greet_person

reveal_type(any_greet_person)
reveal_type(typed_greet_person)

请记住,被定义为any_greet_person的对象是一种特定类型,创建后无法简单删除。


为了使用特定类型 创建 可调用对象,可以从模板对象中复制它(抽象类型CallableProtocol不能与Type[C]一起使用)。这可以通过装饰器实现:

from typing import TypeVar, Callable

C = TypeVar('C', bound=Callable)  # parameterize over all callables

def copy_signature(template: C) -> Callable[[C], C]:
    """Decorator to copy the static signature between functions"""
    def apply_signature(target: C) -> C:
        # copy runtime inspectable metadata as well
        target.__annotations__ = template.__annotations__
        return target
    return apply_signature

这也编码了只有与复制的签名兼容的函数才是有效目标。

# signature template
def greetable(name: str, age: int) -> str: ...

@copy_signature(greetable)
def any_greet_person(name, age): ...

@copy_signature(greetable)  # error: Argument 1 has incompatible type ...
def not_greet_person(age, bar): ...

print(any_greet_person.__annotations__)  # {'name': <class 'str'>, 'age': <class 'int'>, 'return': <class 'str'>}
if TYPE_CHECKING:
    reveal_type(any_greet_person) # note: Revealed type is 'def (name: builtins.str, age: builtins.int) -> builtins.str'

PyCharm 2020.3.2(社区版),采用装饰器方法。 在运行时,print(any_greet_person.annotations)会打印正确的类型,但没有静态类型提示。[参见](image)。 - Eliav Louski
2
@EliavLouski 抱歉,看起来PyCharm内置的类型检查器不如MyPy等工具全面。PyCharm“正确地”知道函数的类型是带有装饰器应用的裸定义 - 但它似乎还没有弄清楚这会影响签名。您可能需要使用其他类型检查器进行验证(如上所述,MyPy有效),或者向IntelliJ提交一个问题单。 - MisterMiyagi
Jetbrains非常重视问题跟踪器上的问题。如果在那里发布(如果尚未解决),他们可能会很快修复所有问题。 - Dustin Wyatt
1
@DustinWyatt - 嗯...我是JetBrains产品的忠实粉丝,但PyCharm在类型提示回归和YouTrack中存在多个未解决的问题。其中许多问题已经存在1-3年之久。他们在维护跟踪器方面做得很好,但根据我的经验,希望能够快速解决这些问题只是一厢情愿。 - olejorgenb
1
我可以报告PyCharm 2022.2 EAP正确理解答案的装饰器 :D - olejorgenb
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接