为*args和**kwargs键入注释

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

例如,如何表示函数的合理参数是一个int或两个int ?type(args)给出Tuple,所以我猜测是将类型注释为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不喜欢函数调用这样做是有道理的,因为它期望在调用本身中有tuple。unpacking后的添加也给出了一个我不理解的输入错误。

如何注释*args**kwargs的合理类型?

173670 次浏览

对于变量位置参数(*args)和变量关键字参数(**kw),您只需要为一个这样的参数指定期望的值。

类型提示 PEP的任意参数列表和默认参数值 section:

任意参数列表也可以被类型注释,这样定义:

def foo(*args: str, **kwds: int): ...

是可接受的,这意味着,例如,所有以下表示函数调用与有效类型的参数:

foo('a', 'b', 'c')
foo(x=1, y=2)
foo('', z=0)

所以你要像这样指定你的方法:

def foo(*args: int):

然而,如果你的函数只能接受一个或两个整数值,你就不应该使用*args,而是使用一个显式的位置参数和第二个关键字参数:

def foo(first: int, second: Optional[int] = None):

现在,函数实际上被限制为一个或两个参数,如果指定的话,两个参数都必须是整数。*args 总是表示0或更多,并且不能被类型提示限制为更具体的范围。

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

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

mypy将其处理为与下面相同的foo的Python 3.5版本:

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

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

首先,一些背景知识。不能对*args的类型进行整体注释,只能对args中项的类型进行注释。所以你不能说*argsTuple[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或类型注释到实际的实现中,它们必须放在最后。

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

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))

目前还不支持

虽然您可以用类型注释可变参数,但我认为它不是很有用,因为它假定所有参数都是相同的类型。

mypy还不支持*args**kwargs的正确类型注释,它们允许分别指定每个可变参数。有一个在mypy_extensions模块上添加Expand帮助器的建议,它将像这样工作:

class Options(TypedDict):
timeout: int
alternative: str
on_error: Callable[[int], None]
on_timeout: Callable[[], None]
...


def fun(x: int, *, **options: Expand[Options]) -> None:
...

GitHub的问题于2018年1月开放,但仍未关闭。注意,虽然问题是关于**kwargs的,但Expand语法也可能用于*args

如果想描述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)

在某些情况下,** warg的内容可以是多种类型。

这似乎对我很管用:

from typing import Any


def testfunc(**kwargs: Any) -> None:
print(kwargs)

from typing import Any, Optional


def testfunc(**kwargs: Optional[Any]) -> None:
print(kwargs)

如果你觉得需要约束**kwargs中的类型,我建议创建一个类似结构的对象,并在那里添加类型。这可以通过数据类或pydantic来实现。

from dataclasses import dataclass


@dataclass
class MyTypedKwargs:
expected_variable: str
other_expected_variable: int




def testfunc(expectedargs: MyTypedKwargs) -> None:
pass

__abc4 __abc0 __abc5 __abc1…__abc6 __abc0 __abc5 __abc1

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

  1. 编写自己的代码 (你可以编辑和更改哪个)
  2. 使用第三方代码 (你不能编辑,或者很难改变)

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

答案取决于你的*args**kwargs是否有均匀类型(即所有相同类型)或异构类型(即不同类型),以及它们是否有固定数量变量/不确定的数量(这里使用的术语是固定的与变量< >强参数数量< / >强)

*args**kwargs有时被用在“__abc2”中。(见下文)。重要的是要理解什么时候这样做,因为它影响你应该输入提示的方式。

最佳实践总是站在巨人的肩膀上:

  • 我强烈建议阅读和研究__ABC0 .pyi存根,特别是标准库,以了解开发人员如何在野外键入这些东西。

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


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

*args

(一)对可变数量的同构参数进行操作

使用*args的第一个原因是要编写一个必须在同质参数的变量(不确定)个数上工作的函数

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

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

例如:在第一种情况下,所有参数都是__abc0或__abc1;在第二种情况下,所有参数都是__abc2。

也可以使用Unions、__abc1、__abc2和Protocols作为*args的类型。

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

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

注意:

使用*args比显式指定参数要少得多 (即从逻辑上讲,您的代码库将有更多不使用的函数 *args )。对同质类型使用*args通常是以避免要求用户 将参数放入 容器< / > 在将它们传递给函数之前

建议输入参数 明确< / > 可能的。< /强> < / p >

还要注意,args是一个元组,因为开箱操作符(*)返回一个元组,所以请注意,你不能直接改变args(你必须从args中取出可变对象)。

(b) 编写装饰符和闭包

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

(c) 调用helper的顶级函数

这是&;__abc2 &;我所提到的。对于Python >= 3.11python文档显示了可以使用TypeVarTuple键入的示例,以便在调用之间保留类型信息。

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

这里,*args中的项有异构类型,可能还有一个可变数量的这两种情况都有问题类型。

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

在类型检查出现之前,开发人员需要检查*args中单个参数的类型(使用assertisinstance等),如果他们需要根据类型进行不同的操作:

例子:

  • 你需要打印传入的__abc0,但对传入的__abc1求和

值得庆幸的是,mypy开发人员包含了类型推断类型窄化mypy来支持这类情况。(此外,如果现有的代码基已经使用__ABC2、__ABC3等来确定*args中项的类型,则不需要做太多更改)

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

  • 将类型__ABC1赋给*args,所以它的元素可以是任何类型,和
  • 在需要的地方使用类型窄化assert ... is (not) Noneisinstanceissubclass等,来确定*args中单个项的类型

1警告:
对于Python >= 3.11*args可以用 TypeVarTuple,但这是意味着在类型提示时使用 可变泛型 < / >。它< >强不应< / >强一般用于输入*args 例子。< / p >

TypeVarTuple主要用于帮助类型提示numpy 数组,tensorflow张量和类似的数据结构,但对于Python >= 3.11,它可以用于在前面所述的调用helper的顶级函数调用之间保存类型信息

处理异构*args(不只是传递)的函数仍然必须类型 狭窄< / > 确定单个项目的类型

对于Python <3.11TypeVarTuple可以通过 typing_extensions,但到目前为止只有通过pyright(不是mypy)对它进行临时支持。此外,PEP 646还包含了使用*args作为类型变量的部分 元组< / > . < / p >


**kwargs

(一)对可变数量的同构参数进行操作

PEP 484支持将**kwargs字典中的所有类型输入为均匀类型。所有都自动是__abc2。

*args一样,也可以使用__abc1、__abc2、Generics和__abc4作为*kwargs的类型。

我还没有找到一个令人信服的用例来处理使用**kwargs均匀命名参数集。

(b) 编写装饰符和闭包

再次,我将指出你在PEP 612中描述的ParamSpec

(c) 调用helper的顶级函数

这也是&;python特有的设计模式"我所提到的。

对于有限的异构关键字类型集,如果PEP 692被批准,则可以使用TypedDictUnpack

然而,同样的事情适用于*args:

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

案例2:(第三方代码)

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


Outtro

静态类型检查器

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

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

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

接口

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

也就是说,在编写接口时,考虑是否应该使用__abc0或__abc1是很重要的。

协议

在OOP中,协议是一个非正式的接口,只在文档中定义,而不是在代码中定义(参见这篇由Luciano Ramalho撰写的Fluent Python第11章的回顾文章)。Python从Smalltalk中采用了这个概念,在Smalltalk中,协议是一个接口,被视为一组要实现的方法。在Python中,这是通过实现特定的dunder方法来实现的,这在Python数据模型中有描述,我简要地提到了在这里

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

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

abc

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

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


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


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

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

Raymond Hettinger在他的PyCon 2019讲座中给出了关于集合模块中的abc的精彩演讲。

此外,Alex Martelli称abc 鹅打字。你可以子类化collections.abc中的许多类,只实现几个方法,并让类的行为像使用dunder方法实现的内置Python协议一样。

 Python Typing Paradigm .

Luciano Ramalho在他的PyCon 2021讲座中对此及其与类型生态系统的关系进行了出色的演讲。

不正确的方法

@overload

@overload被设计用来模拟功能多态性

  • Python本身不支持函数多态性(c++和其他几种语言支持)。

    • 如果你def一个具有多个签名的函数,最后一个函数__abc0d将覆盖(重定义)前面的函数。
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语法并不相关。我仍然在自己的代码中为mypy赋值,所以这在时间上对我来说是一个可以接受的妥协。也许它能帮助某些人。