我正在尝试使用Python的类型注释和抽象基类来编写一些接口。是否有一种方法来注释*args和**kwargs的可能类型?

例如,如何表达一个函数的合理参数是一个整型或两个整型?type(args)给出元组,所以我的猜测是将类型注释为Union[Tuple[int, int], Tuple[int]],但这行不通。

from typing import Union, Tuple

def foo(*args: Union[Tuple[int, int], Tuple[int]]):
    try:
        i, j = args
        return i + j
    except ValueError:
        assert len(args) == 1
        i = args[0]
        return i

# ok
print(foo((1,)))
print(foo((1, 2)))
# mypy does not like this
print(foo(1))
print(foo(1, 2))

来自myypy的错误消息:

t.py: note: In function "foo":
t.py:6: error: Unsupported operand types for + ("tuple" and "Union[Tuple[int, int], Tuple[int]]")
t.py: note: At top level:
t.py:12: error: Argument 1 to "foo" has incompatible type "int"; expected "Union[Tuple[int, int], Tuple[int]]"
t.py:14: error: Argument 1 to "foo" has incompatible type "int"; expected "Union[Tuple[int, int], Tuple[int]]"
t.py:15: error: Argument 1 to "foo" has incompatible type "int"; expected "Union[Tuple[int, int], Tuple[int]]"
t.py:15: error: Argument 2 to "foo" has incompatible type "int"; expected "Union[Tuple[int, int], Tuple[int]]"

myypy不喜欢这个函数调用是有道理的,因为它期望在调用本身中有一个元组。unpacking后的添加也给出了一个我不理解的输入错误。

如何注释*args和**kwargs的敏感类型?


当前回答

如果想描述kwargs中所期望的特定命名参数,则可以传入TypedDict(它定义了必需的和可选的参数)。可选参数是kwarg是什么。 注意:TypedDict在python >= 3.8中 请看这个例子:

import typing

class RequiredProps(typing.TypedDict):
    # all of these must be present
    a: int
    b: str

class OptionalProps(typing.TypedDict, total=False):
    # these can be included or they can be omitted
    c: int
    d: int

class ReqAndOptional(RequiredProps, OptionalProps):
    pass

def hi(req_and_optional: ReqAndOptional):
    print(req_and_optional)

其他回答

如果想描述kwargs中所期望的特定命名参数,则可以传入TypedDict(它定义了必需的和可选的参数)。可选参数是kwarg是什么。 注意:TypedDict在python >= 3.8中 请看这个例子:

import typing

class RequiredProps(typing.TypedDict):
    # all of these must be present
    a: int
    b: str

class OptionalProps(typing.TypedDict, total=False):
    # these can be included or they can be omitted
    c: int
    d: int

class ReqAndOptional(RequiredProps, OptionalProps):
    pass

def hi(req_and_optional: ReqAndOptional):
    print(req_and_optional)

在不改变函数签名的情况下,最简单的方法是使用@overload

首先,一些背景知识。你不能把*args的类型作为一个整体来标注,只能标注args中各项的类型。所以你不能说*args是元组[int, int],你只能说*args中的每一项的类型是int。这意味着你不能限制*args的长度,也不能为每一项使用不同的类型。

为了解决这个问题,你可以考虑改变函数的签名,给它命名参数,每个参数都有自己的类型注释,但如果想(或需要)让你的函数使用*args,你可以使用@overload让mypy工作:

from typing import overload

@overload
def foo(arg1: int, arg2: int) -> int:
    ...

@overload
def foo(arg: int) -> int:
    ...

def foo(*args):
    try:
        i, j = args
        return i + j
    except ValueError:
        assert len(args) == 1
        i = args[0]
        return i

print(foo(1))
print(foo(1, 2))

注意,您没有向实际实现添加@overload或type注释,它们必须放在最后。

您还可以使用它来改变返回的结果,使哪个参数类型与哪个返回类型相对应。例如:

from typing import Tuple, overload

@overload
def foo(arg1: int, arg2: int) -> Tuple[int, int]:
    ...

@overload
def foo(arg: int) -> int:
    ...

def foo(*args):
    try:
        i, j = args
        return j, i
    except ValueError:
        assert len(args) == 1
        i = args[0]
        return i

print(foo(1))
print(foo(1, 2))

我正在尝试使用Python的类型注释和抽象基类来编写一些接口。有没有一种方法来注释*args和**kwargs的可能类型…如何注释*args和**kwargs的敏感类型

提到类型提示,通常有两种用法:

编写自己的代码(可以编辑和更改) 使用第三方代码(您无法编辑或难以更改)

大多数用户都是两者的结合。

答案取决于你的*args和**kwargs是否具有同质类型(即所有相同类型)或异质类型(即不同类型),以及它们的数量是固定的还是可变的/不确定的(这里使用的术语是固定的vs.可变的)。

*args和**kwargs有时被用于我称之为“特定于python的设计模式”(见下文)。重要的是要理解什么时候这样做,因为它影响你应该输入提示的方式。

最好的做法,总是站在巨人的肩膀上:

我强烈建议阅读和研究已排版的.pyi存根,尤其是标准库的存根,以了解开发人员是如何在野外键入这些东西的。

对于那些想要看到如何生活的人,请考虑投票以下pr:

建议:将类型模块pep转换为一系列howto 文档结构和愿望列表


案例1:(编写自己的代码)

* args

(a)作用于可变数量的齐次参数

使用*args的第一个原因是编写一个必须处理变量(不确定)数量的同质参数的函数

例如:对数字求和,接受命令行参数,等等。

在这些情况下,所有*args都是同质的(即所有相同类型)。

示例:在第一种情况下,所有参数都是int型或浮点型;在第二种情况下,所有参数都是strs。

也可以使用联合、类型别名、泛型和协议作为*args的类型。

我声称(没有证据),对不确定数量的同构参数进行操作是*args引入Python语言的第一个原因。

因此,PEP 484支持为*args提供同构类型。

Note: Using *args is done much less often than specifying parameters explicitly (i.e. logically, your code base will have many more functions that don't use *args than do). Using *args for homogeneous types is normally done to avoid requiring users to put arguments into a container before passing them to the function. It is recommended to type parameters explicitly wherever possible. If for nothing else, you would normally be documenting each argument with its type in a docstring anyway (not documenting is a quick way to make others not want to use your code, including your future self.) Note also that args is a tuple because the unpacking operator (*) returns a tuple, so note that you can't mutate args directly (You would have to pull the mutable object out of args).

(b)编写装饰符和闭包

*args会弹出的第二个地方是在装饰器中。为此,使用PEP 612中描述的ParamSpec是正确的方法。

(c)调用helper的顶级函数

这就是我提到的“特定于python的设计模式”。对于Python >= 3.11, Python文档展示了使用TypeVarTuple键入类型的示例,以便在调用之间保留类型信息。

以这种方式使用*args通常是为了减少编写的代码量,特别是当多个函数之间的参数相同时 它还被用于通过元组解包“吸收”可变数量的参数,这些参数在下一个函数中可能不需要

在这里,*args中的项具有异构类型,并且可能有不同数量的异构类型,这两者都可能是有问题的。

Python类型生态系统没有指定异构*args的方法。1

在类型检查出现之前,开发人员需要检查*args中单个参数的类型(使用assert, isinstance等),如果他们需要根据类型做一些不同的事情:

例子:

你需要打印传入的字符串,但对传入的整数求和

值得庆幸的是,mypy开发人员为mypy提供了类型推断和类型缩小功能,以支持这类情况。(同样,如果现有的代码库已经使用assert、isinstance等来确定*args中项的类型,则不需要做太多更改)

因此,在这种情况下,您将执行以下操作:

为*args指定type对象,这样它的元素可以是任何类型 在需要的地方使用assert…is (not) None, isinstance, issubclass,等等,来确定*args中单个项的类型


1 Warning: For Python >= 3.11, *args can be typed with TypeVarTuple, but this is meant to be used when type hinting variadic generics. It should not be used for typing *args in the general case. TypeVarTuple was primarily introduced to help type hint numpy arrays, tensorflow tensors, and similar data structures, but for Python >= 3.11, it can be used to preserve type information between calls for top-level functions calling helpers as stated before. Functions that process heterogenous *args (not just pass them through) must still type narrow to determine the types of individual items. For Python <3.11, TypeVarTuple can be accessed through typing_extensions, but to date there is only provisional support for it through pyright (not mypy). Also, PEP 646 includes a section on using *args as a Type Variable Tuple.


* * kwargs

(a)作用于可变数量的齐次参数

PEP 484支持将**kwargs字典的所有值输入为同构类型。所有键都自动为字符串。

像*args一样,也可以使用联合、类型别名、泛型和协议作为*kwargs的类型。

我还没有发现使用**kwargs处理同质命名参数集的令人信服的用例。

(b)编写装饰符和闭包

我再次建议您参考PEP 612中描述的ParamSpec。

(c)调用helper的顶级函数

这也是我提到的“特定于python的设计模式”。

对于有限的异构关键字类型集,如果PEP 692得到批准,则可以使用TypedDict和Unpack。

然而,*args同样适用于这里:

最好显式地输入关键字参数 如果你的类型是异构的并且大小未知,在函数体中用object和narrow类型提示类型

案例2:(第三方代码)

这最终相当于遵循案例1中(c)部分的指导方针。


Outtro

静态类型检查器

问题的答案还取决于您使用的静态类型检查器。到目前为止(据我所知),你对静态类型检查器的选择包括:

mypy: Python事实上的静态类型检查器 pyright:微软的静态类型检查器 pyre: Facebook/Instagram的静态类型检查器 pytype:谷歌的静态类型检查器

我个人只使用过myypy和pyright。对于这些,mypy playground和pyright playground是测试代码类型提示的好地方。

接口

abc,就像描述符和元类一样,是构建框架的工具(1)。如果你有机会把你的API从“成年人同意”的Python语法变成“约束和纪律”语法(借用Raymond Hettinger的短语),考虑YAGNE。

也就是说,在编写接口时,考虑应该使用协议还是abc是很重要的。

协议

在面向对象编程中,协议是一种非正式的接口,只在文档中定义,而不是在代码中定义(参见Luciano Ramalho撰写的Fluent Python第11章评论文章)。Python从Smalltalk中采用了这个概念,在Smalltalk中,协议是一个接口,被视为一组要实现的方法。在Python中,这是通过实现特定的dunder方法来实现的,这在Python数据模型中有描述,我在这里简单介绍一下。

协议实现了所谓的结构子类型。在这个范例中,_a子类型是由它的结构(即行为)决定的,而不是名义子类型(即子类型是由它的继承树决定的)。与传统的(动态的)鸭子类型相比,结构子类型也称为静态鸭子类型。(这个词是亚历克斯·马特利(Alex Martelli)发明的。)

其他类不需要子类化来遵循协议:它们只需要实现特定的dunder方法。通过类型提示,Python 3.8中的PEP 544引入了一种形式化协议概念的方法。现在,您可以创建一个继承自Protocol的类,并在其中定义您想要的任何函数。只要另一个类实现了这些函数,它就被认为遵守了该协议。

ABCs

抽象基类是duck类型的补充,当你遇到以下情况时很有帮助:

class Artist:
    def draw(self): ...

class Gunslinger:
    def draw(self): ...

class Lottery:
    def draw(self): ...

在这里,这些类都实现了一个draw(),这一事实并不一定意味着这些对象是可互换的(再次强调,参见Fluent Python,第11章,by Luciano Ramalho)!ABC使您能够明确地声明意图。此外,您还可以通过注册类来创建一个虚拟子类,这样就不必从它继承子类(从这个意义上讲,通过不直接绑定ABC,您遵循了“优先组合而不是继承”的GoF原则)。

Raymond Hettinger在他的PyCon 2019 talk中就collections模块的abc进行了精彩的演讲。

此外,Alex Martelli称abc为鹅打字。您可以子类化集合中的许多类。abc,只实现了几个方法,并让类的行为类似于用dunder方法实现的内置Python协议。

Luciano Ramalho在他的PyCon 2021 talk中就这一点及其与类型生态系统的关系进行了精彩的演讲。

不正确的方法

@overload

@overload被设计用来模拟函数多态性。

Python本身不支持函数多态性(c++和其他几种语言支持)。 如果你定义了一个有多个签名的函数,最后一个被定义的函数将覆盖(重定义)前一个。

def func(a: int, b: str, c: bool) -> str:
    print(f'{a}, {b}, {c}')

def func(a: int, b: bool) -> str:
    print(f'{a}, {b}')

if __name__ == '__main__':
    func(1, '2', True)  # Error: `func()` takes 2 positional arguments but 3 were given

Python用可选的位置/关键字参数模拟函数多态性(巧合的是,c++不支持关键字参数)。

重载用于

(1)输入移植的C/ c++多态函数,或者 (2)根据函数调用中使用的类型,类型之间必须保持一致性

请参阅Adam Johnson的博客文章“Python类型提示-如何使用@overload”。

参考文献

(1) Ramalho, Luciano。流利的Python(第320页)。O ' reilly媒体。Kindle版。

博士TL;

def __init__(self, *args, **kwargs):  # type: ignore[no-untyped-def]

动机

这是Chris在评论中给出的答案,我在扫描答案的5分钟内没有找到共识,而且对我来说,正确输入这种默认的Python语法并不相关。但我仍然重视我自己代码上的ypy,所以从时间上看,这对我来说是一个可以接受的妥协。也许它能帮助某些人。

作为前面答案的简短补充,如果你试图在Python 2文件上使用mypy,并且需要使用注释而不是注释来添加类型,你需要分别在args和kwargs的类型前面加上*和**:

def foo(param, *args, **kwargs):
    # type: (bool, *str, **int) -> None
    pass

这被mypy视为与下面相同,Python 3.5版本的foo:

def foo(param: bool, *args: str, **kwargs: int) -> None:
    pass