*args和**kwargs的类型注释

486

我正在尝试使用抽象基类来编写一些接口,以 Python 的类型注释。有没有办法注释*args**kwargs的可能类型?

例如,如何表达函数的合理参数是一个int或两个inttype(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))

mypy的错误信息:

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]]"

对于函数调用,mypy不喜欢这个是有道理的,因为它希望在调用本身中存在一个tuple。拆包后的加法也会产生一个我不理解的类型错误。

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

8个回答

500
*args**kw变量位置参数只需要指定一个参数的预期值。
从PEP中的Type Hints部分的任意参数列表和默认参数值部分:

任意参数列表也可以进行类型注释,因此定义:

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个或更多个参数,并且无法通过类型提示限制为更具体的范围。


4
只是好奇,为什么要添加“Optional”?Python 有什么变化还是你改变了主意?由于“None”默认值,它仍然不是严格必需的吗? - Praxeolitic
24
是的,在实际使用中,当您使用“None”作为默认值时,自动隐含的“Optional”注释会使某些用例更加困难,该功能现在被从 PEP 中移除了。 - Martijn Pieters
14
对于那些感兴趣的人,这里有一个讨论此问题的链接。显然,将来需要使用显式的Optional - Rick
3
@ShitalShah:那不是问题的关键。Callable根本不支持对*args**kwargs进行任何类型提示的提及,完全没有。该特定问题涉及标记接受特定参数加上任意数量其他参数的可调用项,并因此使用*args: Any, **kwargs: Any,这是两个catch-all的非常特定的类型提示。对于将*args和/或**kwargs设置为更具体内容的情况,可以使用Protocol - Martijn Pieters
1
@wjandrea 这里过载并不会真正帮助,使用过载来帮助类型检查器,当参数的组合改变返回值时;例如foo(1)返回一个字符串,但foo(1, 2)产生一个整数等。 - Martijn Pieters
显示剩余2条评论

96

2022更新

mypy团队已经添加了对Unpack的支持,此功能可在Mypy 0.981或更高版本中使用。

注意!尽管此功能已经完善,但Unpack[...]仍被视为实验性功能,因此您需要使用--enable-incomplete-features来启用它。

您可以按照以下方式使用此功能:

from typing import TypedDict
from typing_extensions import Unpack


class RequestParams(TypedDict):
    url: str
    allow_redirects: bool


def request(**kwargs: Unpack[RequestParams]) -> None:
    ...

如果你使用在 TypedDict 中定义的参数调用 request 函数,你将不会收到任何错误。
# OK
request(url="https://example.com", allow_redirects=True)

如果你忘记传递参数,mypy现在会提醒你。
# error: Missing named argument "allow_redirects" for "request"  [call-arg]
request(url="https://example.com")

你还可以通过在TypedDict中添加total=False来使字段变为非必填项。
class RequestParams(TypedDict, total=False):
    url: str
    allow_redirects: bool

# OK
request(url="https://example.com")

或者,您可以使用RequiredNotRequired注释来控制关键字参数是否是必需的或非必需的。
from typing import TypedDict
from typing_extensions import Unpack, NotRequired


class RequestParams(TypedDict):
    url: str
    allow_redirects: NotRequired[bool]

def request(**kwargs: Unpack[RequestParams]) -> None:
    ...

# OK
request(url="https://example.com", allow_redirects=True)

以下是旧答案:

虽然你可以给可变参数注释类型,但我觉得这并不是很有用,因为它假设所有的参数都是相同的类型。

目前,对*args**kwargs进行逐个指定类型注释的正确方式还不被mypy支持。有一个提案,即在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 issue 是在2018年1月份被打开的,但是现在还没有关闭。请注意,虽然这个问题是关于 **kwargs 的,但是 Expand 语法很可能也会用于 *args


3
根据 https://github.com/microsoft/pyright/issues/3002#issuecomment-1046100462 的说法,新的语法是 **options: Unpack[Options],可在 Pylance 中使用(但尚未在 mypy 中使用)。 - rattray
1
太好了。如果答案是:# type: ignore[no-untyped-def],那就是答案! - Chris
3
@Chris 我认为这是本帖唯一的答案,也是我在“python-typing”标签下找到的最有用的答案之一。 - bad_coder
这现在是Python 3.12的一部分。 - undefined

55

在不改变函数签名的情况下,最简单的方法是使用@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))

2
我喜欢这个答案,因为它涉及到了更一般的情况。回过头来看,我不应该以(type1)(type1, type1)函数调用作为我的例子。也许(type1)(type2, type1)会是一个更好的例子,并且说明了我为什么喜欢这个答案。这也允许有不同的返回类型。然而,在特殊情况下,当你只有一个返回类型并且你的*args*kwargs都是相同类型时,Martjin的答案中的技巧更有意义,所以两个答案都很有用。 - Praxeolitic
13
即使在参数个数有最大限制(这里是2个)的情况下使用 *args 仍然是错误的。 - Martijn Pieters
2
所以,是的,了解@overload是很好的,但它不是这个特定工作的正确工具。 - Martijn Pieters
4
*argsе®һйҷ…дёҠжҳҜз”ЁдәҺдј йҖ’йӣ¶дёӘжҲ–еӨҡдёӘеҗҢзұ»еҸӮж•°пјҢжІЎжңүж•°йҮҸйҷҗеҲ¶пјҢжҲ–иҖ…з”ЁдәҺдј йҖ’жңӘз»ҸеӨ„зҗҶзҡ„'catch-all'еҸӮж•°гҖӮдҪ жңүдёҖдёӘеҝ…йңҖеҸӮж•°е’ҢдёҖдёӘеҸҜйҖүеҸӮж•°гҖӮиҝҷжҳҜе®Ңе…ЁдёҚеҗҢзҡ„жғ…еҶөпјҢйҖҡеёёйҖҡиҝҮдёә第дәҢдёӘеҸӮж•°жҢҮе®ҡдёҖдёӘзү№ж®Ҡй»ҳи®ӨеҖјжқҘжЈҖжөӢжҳҜеҗҰзңҒз•ҘдәҶе®ғгҖӮ - Martijn Pieters
6
看完PEP后,很明显这不是@overload的预期用法。虽然这个答案展示了一种有趣的逐个注释*args类型的方式,但更好的回答是根本不应该这样做。 - Praxeolitic
显示剩余12条评论

28

对于之前的回答,如果您尝试在Python 2文件上使用mypy并需要使用注释来添加类型而不是注解,则需要分别为argskwargs的类型加上前缀***

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

1
这个页面现在有多个答案。您能明确地提到之前的哪个答案吗? - Olúwátóósìn Anímáṣahun

9

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

对我来说,这似乎有效:

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 中的类型,我建议创建一个类似于结构体的对象,并在那里添加类型。可以使用 dataclasses 或 pydantic 进行操作。

from dataclasses import dataclass

@dataclass
class MyTypedKwargs:
   expected_variable: str
   other_expected_variable: int


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

这基本上禁用了类型检查,不是吗?这就像完全省略kwargs的注释一样。 - normanius
6
**kwargs 是按设计而来的,技术上可以是任何东西。如果您知道自己要获取什么信息,建议将其定义为有类型的参数。这里的优点在于,在像 PyCharm 这样的 IDE/工具中,对于使用 **kwargs 是可接受/预期的情况,它不会提醒您类型不正确了。 - monkut
1
我有些不同意。我认为在某些情况下,对于kwargs或*args限制类型是合理的。但我也看到类型检查和kwargs不太搭配(至少对于当前的Python版本)。也许你想把这个加入到你的答案中,以更好地回答OP的问题。 - normanius
2
是的,或许在输入kwargs时有一些用例,但我更倾向于使您的输入更清晰,而不是将它们混合到kwargs中。 - monkut
1
避免使用 Any 是一个好的实践,因为它完全禁用了类型检查。相反,您可以使用 object,然后在扩展 kwargs 的任何地方使用 # type: ignore - V13

8
我正在尝试使用抽象基类和Python的类型注释编写一些接口。有没有一种方法可以注释*args**kwargs的可能类型...如何注释*args**kwargs的合理类型是什么?

当涉及到类型提示时,有两个常见的用法类别:

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

大多数用户都有这两种组合。

答案取决于您的*args**kwargs是否具有同质类型(即全部为相同类型)或异质类型(即不同类型),以及它们是否有固定数量可变/不确定数量(此处使用的术语是固定 vs. 可变 arity)。 *args**kwargs有时被用于我松散称之为“Python特定设计模式”的内容中(见下文)。了解何时这样做很重要,因为它会影响您应该输入的方式。
最佳实践始终是站在巨人的肩膀上
  • 我强烈建议阅读和学习typeshed .pyi存根文件, 特别是标准库,以了解开发人员如何在实践中对这些内容进行类型定义。

对于那些想要看到 HOW-TO 实现的人,请考虑投票支持以下 PR:


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

*args

(a) 操作数量不定的同类参数

使用*args的第一个原因是编写一个必须在数量不定的同类参数上工作的函数

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

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

例如: 在第一个例子中,所有的参数都是intfloat; 在第二个例子中,所有的参数都是str.

也可以将Union, TypeAlias, GenericProtocol用作*args的类型。

我声称(未经证明)操作数量不定的同类参数是*args被引入Python语言中的第一个原因。

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

注意:

使用 *args 的情况比直接指定参数要少得多(即逻辑上,你的代码库中使用 *args 的函数要比不使用的多)。对于同种类型的参数,通常会使用 *args 来避免要求用户在将它们传递给函数之前将它们放入容器中。

建议在可能的情况下明确指定参数类型。

  • 即使没有其他原因,你通常也会在 docstring 中记录每个参数的类型(不记录是让别人不想使用你的代码的快速方式,包括你未来的自己)

还要注意,args 是一个元组,因为解包运算符(*返回一个元组,所以请注意,你不能直接改变 args(你必须先将可变对象从 args 中取出)。

(b) 写装饰器和闭包 在装饰器中,*args 的第二个用途是使用 PEP 612 中描述的 ParamSpec(c) 调用助手函数的顶层函数 这是我提到的“Python特定的设计模式”。对于Python >= 3.11,Python文档中显示了示例,您可以使用TypeVarTuple 对其进行类型化,以便在调用之间保留类型信息。
  • 使用这种方式的*args通常是为了减少编写的代码量,尤其是当多个函数之间的参数相同时
  • 它还被用来通过元组展开“吞噬”可能不需要在下一个函数中使用的可变数量的参数

这里,*args 中的项目具有不同的类型,并且可能数量可变,这两个问题都可能会引起问题

Python 类型系统无法指定异构的 *args1

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

例如:

  • 您需要打印传递的 str,但汇总传递的 int
幸运的是,mypy 的开发者们在 mypy 中加入了类型推断类型缩小来支持这些情况。(此外,如果已经使用assertisinstance等方法来确定*args中项的类型,那么现有的代码库不需要做太多更改。)
因此,在这种情况下,您将执行以下操作:

1 警告:

对于 Python >= 3.11*args 可以使用 TypeVarTuple 进行类型注释,但这是用于注释可变泛型的,不应该用于一般情况下的 *args 类型注释。

TypeVarTuple 主要用于帮助类型注释 numpy 数组、tensorflow 张量和类似的数据结构,在 Python >= 3.11 中,它可以用于在 调用辅助函数的顶层函数 之间保留类型信息。

处理异构的 *args 函数(不仅仅是传递)仍然必须进行类型缩小以确定每个项的类型。

对于 Python <3.11TypeVarTuple 可以通过 typing_extensions 访问,但迄今为止,只有 pyright(而不是 mypy)对其进行了临时支持。此外,PEP 646 包括了一个关于使用*args 作为类型变量元组的部分。


**kwargs

(a) 操作可变数量的同质参数

PEP 484 支持将 **kwargs 字典中的所有都定义为同质类型。所有自动转换为 str

*args 类似,也可以使用 UnionTypeAliasGenericProtocol 作为 *kwargs 的类型。

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

(b) 编写装饰器和闭包

同样,我会指向 ParamSpec,如 PEP 612 中所述。

(c) 调用帮助程序的顶层函数

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

对于一组有限的异构关键字类型,如果PEP 692得到批准,可以使用TypedDictUnpack

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

  • 最好明确地为您的关键字参数添加类型注释
  • 如果您的类型是异构的且大小未知,请使用object进行类型提示,并在函数体中进行类型缩小

情况2:(第三方代码)

这本质上等同于遵循Case 1中的(c)部分的指南。


结束语

静态类型检查器

回答你的问题还取决于你使用的静态类型检查器。迄今为止(据我所知),你可以选择以下静态类型检查器:

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

就个人而言,我只用过mypypyright。对于这些工具,mypy playgroundpyright playground 是测试代码类型提示的好地方。

接口

ABCs、描述符和元类都是构建框架的工具(1)。如果有可能将API从“成年人同意”的Python语法转换为“束缚和纪律”语法(借用Raymond Hettinger的话),请考虑使用YAGNE
话虽如此(除了说教),在编写接口时,考虑是否应该使用ProtocolABC是很重要的。
协议
在面向对象编程中,协议是一种非正式的接口,仅在文档中定义而不是在代码中定义(请参见Luciano Ramalho撰写的Fluent Python第11章的评论文章)。Python从Smalltalk采用了这个概念,其中协议被视为要实现的方法集合。在Python中,这通过实现特定的dunder方法来实现,这在Python数据模型中有描述,我在此处简要介绍。

协议实现了所谓的结构子类型。在这种范式中,一个子类型是由其结构(即行为)决定的,而不是由名义子类型(即子类型由其继承树确定)决定。与传统(动态的)鸭子类型相比,结构子类型也称为静态鸭子类型。 (该术语归功于Alex Martelli。)

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

抽象基类

抽象基类和鸭子类型相辅相成,在遇到以下情况时非常有用:

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

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

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

在这里,这些类都实现了一个draw(),但这并不一定意味着这些对象是可互换的(请参见Luciano Ramalho的《流畅的Python》第11章)!抽象基类使您能够清晰地声明意图。此外,您可以通过register注册该类来创建一个虚拟子类,因此您不必从中进行子类化(在这个意义上,您遵循了GoF“优先使用组合而不是继承”的原则,而不是直接将自己与ABC绑定)。Raymond Hettinger在他的PyCon 2019 Talk中对collections模块中的ABCs进行了精彩的讲解。此外,Alex Martelli称ABC为鹅类型。您可以对collections.abc中的许多类进行子类化,只实现少量方法,并使类像使用dunder方法实现的内置Python协议一样运行。

The Python Typing Paradigm

Luciano Ramalho在他的PyCon 2021 Talk中对此进行了精彩的讲解,并介绍了与类型生态系统的关系。

不正确的方法

@overload

@overload旨在用于模仿functional polymorphism

  • Python不支持函数式多态(C++和其他几种语言支持)。

    • 如果您使用多个签名定义一个函数,最后一个def的函数会覆盖(重新定义)之前的函数。
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 Type Hints - How to Use @overload

参考资料

(1) Ramalho, Luciano. Fluent Python (p. 320). O'Reilly Media. Kindle Edition.


7
如果想要描述kwargs中期望的具体命名参数,可以传入TypedDict(定义必需和可选参数)。可选参数是kwargs中的内容。 注意: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)

1
有时候你会陷入一种做事情的方式中,而忘记了简单的方法。谢谢你提醒我。 - Edward
这太棒了。+1 - Federico Baù
这似乎不起作用。 - sanders
哪一部分出了问题?这取决于使用Python 3.8或更高版本。你是在说IDE没有实现TypedDict支持吗? - spacether

4

简而言之

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

动机

这是Chris在评论中给出的答案,我在扫描答案5分钟内没有找到共识,并且对于我来说,正确输入此默认Python语法并不那么重要。尽管如此,我仍然重视mypy在我的代码中的作用,因此从时间上讲,这对我来说是一个可以接受的妥协。也许它能帮助某些人。


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