我正在尝试使用抽象基类和Python的类型注释编写一些接口。有没有一种方法可以注释
*args
和
**kwargs
的可能类型...如何注释
*args
和
**kwargs
的合理类型是什么?
当涉及到类型提示时,有两个常见的用法类别:
- 编写自己的代码(您可以编辑和更改)
- 使用第三方代码(您无法编辑或难以更改)
大多数用户都有这两种组合。
答案取决于您的
*args
和
**kwargs
是否具有
同质类型(即全部为相同类型)或
异质类型(即不同类型),以及它们是否有
固定数量或
可变/不确定数量(此处使用的术语是固定 vs. 可变
arity)。
*args
和
**kwargs
有时被用于我松散称之为“Python特定设计模式”的内容中(见下文)。了解何时这样做很重要,因为它会影响您应该输入的方式。
最佳实践始终是
站在巨人的肩膀上:
对于那些想要看到 HOW-TO 实现的人,请考虑投票支持以下 PR:
案例1: (编写自己的代码)
*args
(a) 操作数量不定的同类参数
使用*args
的第一个原因是编写一个必须在数量不定的同类参数上工作的函数
例如: 对数字求和, 接受命令行参数等.
在这些情况下,所有的*args
都是同类的 (即都是相同类型).
例如: 在第一个例子中,所有的参数都是int
或float
; 在第二个例子中,所有的参数都是str
.
也可以将Union
, TypeAlias
, Generic
和Protocol
用作*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 类型系统无法指定异构的 *args
。1
在类型检查出现之前,如果开发人员需要根据类型执行不同的操作,则需要检查 *args
中各个参数的类型(使用 assert
、isinstance
等):
例如:
幸运的是,
mypy
的开发者们在
mypy
中加入了
类型推断和
类型缩小来支持这些情况。(此外,如果已经使用
assert
、
isinstance
等方法来确定
*args
中项的类型,那么现有的代码库不需要做太多更改。)
因此,在这种情况下,您将执行以下操作:
- 将
*args
的类型设置为object
,这样它的元素可以是任何类型,并且
- 在需要时使用类型缩小,使用
assert ... is (not) None
,isinstance
,issubclass
等来确定*args
中各个项的类型。
1 警告:
对于 Python >= 3.11
,*args
可以使用 TypeVarTuple
进行类型注释,但这是用于注释可变泛型的,不应该用于一般情况下的 *args
类型注释。
TypeVarTuple
主要用于帮助类型注释 numpy
数组、tensorflow
张量和类似的数据结构,在 Python >= 3.11
中,它可以用于在 调用辅助函数的顶层函数 之间保留类型信息。
处理异构的 *args
函数(不仅仅是传递)仍然必须进行类型缩小以确定每个项的类型。
对于 Python <3.11
,TypeVarTuple
可以通过 typing_extensions
访问,但迄今为止,只有 pyright
(而不是 mypy
)对其进行了临时支持。此外,PEP 646
包括了一个关于使用*args
作为类型变量元组的部分。
**kwargs
(a) 操作可变数量的同质参数
PEP 484
支持将 **kwargs
字典中的所有值都定义为同质类型。所有键自动转换为 str
。
与 *args
类似,也可以使用 Union
、TypeAlias
、Generic
和 Protocol
作为 *kwargs
的类型。
我没有找到处理使用 **kwargs
的同质命名参数集的令人信服的用例。
(b) 编写装饰器和闭包
同样,我会指向 ParamSpec
,如 PEP 612
中所述。
(c) 调用帮助程序的顶层函数
这也是我提到的“Python特定设计模式”。
对于一组有限的异构关键字类型,如果PEP 692得到批准,可以使用TypedDict
和Unpack
。
然而,对于*args
的同样适用:
- 最好明确地为您的关键字参数添加类型注释
- 如果您的类型是异构的且大小未知,请使用
object
进行类型提示,并在函数体中进行类型缩小
情况2:(第三方代码)
这本质上等同于遵循Case 1
中的(c)
部分的指南。
结束语
静态类型检查器
回答你的问题还取决于你使用的静态类型检查器。迄今为止(据我所知),你可以选择以下静态类型检查器:
mypy
:Python的事实上的静态类型检查器
pyright
:微软的静态类型检查器
pyre
:Facebook/Instagram的静态类型检查器
pytype
:Google的静态类型检查器
就个人而言,我只用过mypy
和pyright
。对于这些工具,mypy
playground 和 pyright
playground 是测试代码类型提示的好地方。
接口
ABCs、描述符和元类都是构建框架的工具(1)。如果有可能将API从“成年人同意”的Python语法转换为“束缚和纪律”语法(借用Raymond Hettinger的话),请考虑使用
YAGNE。
话虽如此(除了说教),在编写接口时,考虑是否应该使用
Protocol
或
ABC
是很重要的。
协议
在面向对象编程中,
协议是一种非正式的接口,仅在文档中定义而不是在代码中定义(请参见
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](https://istack.dev59.com/53yfx.webp)
Luciano Ramalho在他的PyCon 2021 Talk中对此进行了精彩的讲解,并介绍了与类型生态系统的关系。
不正确的方法
@overload
@overload
旨在用于模仿functional polymorphism。
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)
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.
Optional
。 - RickCallable
根本不支持对*args
或**kwargs
进行任何类型提示的提及,完全没有。该特定问题涉及标记接受特定参数加上任意数量其他参数的可调用项,并因此使用*args: Any, **kwargs: Any
,这是两个catch-all的非常特定的类型提示。对于将*args
和/或**kwargs
设置为更具体内容的情况,可以使用Protocol
。 - Martijn Pietersfoo(1)
返回一个字符串,但foo(1, 2)
产生一个整数等。 - Martijn Pieters