早先对 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 函数参数
首先 args
和 kwargs
是两个参数命名的习俗,按自己的喜爱可以用任意的词,如 *aaa
, **bbb
,不过最好是按照大家的习惯来命名。
*
和 **
可用于拆解参数,如
思考并复习上一节的内容:
- 如果定义 def foo(*, a, b, c) 是否能用 foo(*[2,3,1]) 来调用呢?
- 如果定义 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
参数的使用,基于 *
, **
分别是列表和字典的拆解,得到下面的理解
- 看到
*args
参数,我们要传入一个被拆解的列表,如 2,3,1。已有列表需拆解后传入, 如 foo(*[2,3,1]) - 看到
**kwargs
参数,我们要传入一个被拆解的字典,如 a=2,c=3,b=1。已有字典需拆解后传入, 如 foo(**{"a":2, "c":3, "b":1})
*args
和 **kwargs
任意一种方式都可以帮我创建无限行为的函数, 通中会结合二者来向一个未知函数传递所有的参数
再一个较典型的例子就是装饰器中,因为装饰器需要由外部的装饰器函数调用另一个被装饰的函数,而被修饰的函数的参数是不定的
这是摘自我之前 熟悉和应用 Python 的装饰器 一文中的例子
链接:
本文链接 https://yanbin.blog/python-ellipsis-star-slash-args-kwargs/, 来自 隔叶黄莺 Yanbin Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。
[…] 由 Python 的 Ellipsis 到 *, /, *args, **kwargs 函数参数, 又回想起在 熟悉和应用 Python […]