我正在尝试使用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版。