由 Python 的 Ellipsis 到 *, /, *args, **kwargs 函数参数

早先对 Python *args, **kwargs 参数有所了解,也知道参数列表中的 / 表示 Positional Only, * 很少见。然而在使用 FastAPI 时看到路由函数中表示默认值采用了 ... 的方式又重新激发起我对 Python 函数参数的 *, /, *args, 和 **kwargs 的兴趣。

如 FastAPI 官方文档 Request Forms and Files 中的

@app.post("/files/")
async def create_file(file: bytes = File(...), fileb: UploadFile = File(...), token: str = Form(...)):

默认值的 File(...), Form(...), 起初还以为 ... 只是真正意义上的省略号,使用时需传入适当的参数,后来发现 ... 居然是一个 Python 实实在在的内置对象。

>>> ...
Ellipsis
>>> id(...)
4473082960
>>> id(...)
4473082960
>>> bool(...)
True
>>> type(...)
<class 'ellipsis'>
 

... 是一个单例的 Ellipsis 对象,它的 bool 值为 True。

查看 FastAPI 的 Form 类

Form(...) 应该是把 ... 传给了第一个参数 default: Any. 验证一下

这样看 ... 没什么特别的,纯粹就是一个 Python 常量。

... 在 Type Hints 中的使用

不定长的 Tuple, 可用 Ellipsis 指定

上面代码用 mypy test.py 校验会出报错

test.py:4: error: Incompatible return value type (got "Tuple[int, int, int]", expected "Tuple[int, int]")
Found 1 error in 1 file (checked 1 source file)

但在函数中可因不用条件返回不同长度的 Tuple, 这时就得用 ..., 写成

mypy test.py 检验通过。

如果返回的值是 (1, 2, 'str') 也是不行的,错误是

test.py:4: error: Incompatible return value type (got "Tuple[int, int, str]", expected "Tuple[int, ...]")
Found 1 error in 1 file (checked 1 source file)

Callable 类型提示时第一个参数必须是 ..., 假如写下面的代码

自己设想 Callable 就一个 int 的参数,不过 mypy test.py 的说法是

test.py:3: error: The first argument to Callable must be a list of types or "..."
Found 1 error in 1 file (checked 1 source file)

Callable[..., int] 的第一个位置上还必须为 ..., 想明确也不行,所以应该写成

这时候你的回调函数写多少个参数都行,如 return lambda x, y: 1 也能通过 mypy 这一关。

特殊函数参数 /*

当单独的 /* 出现在函数参数中,它们不是用来接收参数值,而是用以界定哪些参数只能按位置传递,哪些参数只能用关键传递,哪些既能用位置又能用关键字传递。

比如对于一个常见的函数声明

def foo(a, b, c, d=0)

我们的调用方式是随意的

foo(1, 2, 3, 4)
foo(1, 2, c=3, 4)       # 这是错误的,一旦开始了以关键字传递参数,则后面的参数都必须指定关键字来传递
foo(b=2, a=1, c=3)

Python 函数中没有默认值的参数要放前面,它们渴望通过位置对应来传递,如果第一个参数用了关键字,后面的参数都必须以同样的方式,否则顺序就会错乱。报的错误信息是

SyntaxError: positional argument follows keyword argument

看 Python 官方对 /* 的解释,见 Special parameters

简单的规则就是: / 前只能按位置传递 (Positional-Only), * 后只能按关键字传递 (Keyword-Only),/ 后 和 * 前(中间)的随意。因为 / 管前面,* 管后面,所以一个函数中同时有 /* 时,/ 必须写成 * 前面。

在 FastAPI 框架中声明函数常用 *, 它的一个约定是对于必需的无默认值的参数放在 * 前面,可用位置或关键字来传递,相应的有默认值的参数放在 * 后面作为可选参数,我们也可以借鉴这种用法。

下面代码逐个验证 /* 的功能

*args, **kwargs 函数参数

首先 argskwargs 是两个参数命名的习俗,按自己的喜爱可以用任意的词,如 *aaa, **bbb,不过最好是按照大家的习惯来命名。

*** 可用于拆解参数,如

思考并复习上一节的内容:

  1. 如果定义 def foo(*, a, b, c) 是否能用 foo(*[2,3,1]) 来调用呢?
  2. 如果定义 def foo(a, b, c, /) 是否能用 foo(**{"a":2, "b":3, "c":1}) 来调用呢?

因此,基于 *** 分别拆解对应的列表和字典的行为,当它们被安放在函数参数名前面也是一样的用法。

*args 相当于是可变参数, 在函数中对应一个列表,在 Java 中就是 Object... args

args 是一个列表,怎么去理解这一个调用过程呢?传入单个值的,在函数中变成了一个列表,看着想是 * 拆解的逆过程。可以这么理解,把 *args 整体看作一个参数,那么可以想像该参数 *args 是一个已拆解的列表,那么去掉 * 号的 args 就是列表本身。所以传参时是这么对应的

单个值的 2,3,1 => *args, 而不是 args

类似的 **kwargs 就是不定数量的按关键字传递的参数

同样的理解方式,把 **kwargs 整体当作一个参数,传参时对应关系就是

a=2, c=3, b=1  => **kwargs, 已拆解的字典

小结一下 *args**kwargs  参数的使用,基于 *, ** 分别是列表和字典的拆解,得到下面的理解

  1. 看到 *args 参数,我们要传入一个被拆解的列表,如 2,3,1。已有列表需拆解后传入, 如 foo(*[2,3,1])
  2. 看到 **kwargs 参数,我们要传入一个被拆解的字典,如 a=2,c=3,b=1。已有字典需拆解后传入, 如 foo(**{"a":2, "c":3, "b":1})

*args**kwargs 任意一种方式都可以帮我创建无限行为的函数, 通中会结合二者来向一个未知函数传递所有的参数

再一个较典型的例子就是装饰器中,因为装饰器需要由外部的装饰器函数调用另一个被装饰的函数,而被修饰的函数的参数是不定的

这是摘自我之前 熟悉和应用 Python 的装饰器 一文中的例子

链接:

  1. Python 的 Ellipsis 对象

本文链接 https://yanbin.blog/python-ellipsis-star-slash-args-kwargs/, 来自 隔叶黄莺 Yanbin Blog

[版权声明] Creative Commons License 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。

Subscribe
Notify of
guest

1 Comment
Inline Feedbacks
View all comments
trackback

[…] 由 Python 的 Ellipsis 到 *, /, *args, **kwargs 函数参数, 又回想起在 熟悉和应用 Python […]