由 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 类
 1def Form(  # noqa: N802
 2    default: Any,
 3    *,
 4    media_type: str = "application/x-www-form-urlencoded",
 5    alias: Optional[str] = None,
 6    title: Optional[str] = None,
 7    description: Optional[str] = None,
 8    gt: Optional[float] = None,
 9    ge: Optional[float] = None,
10    lt: Optional[float] = None,
11    le: Optional[float] = None,
12    min_length: Optional[int] = None,
13    max_length: Optional[int] = None,
14    regex: Optional[str] = None,
15    example: Any = Undefined,
16    examples: Optional[Dict[str, Any]] = None,
17    **extra: Any,
18) -> Any:

Form(...) 应该是把 ... 传给了第一个参数 default: Any. 验证一下
1>>> def foo(a, *, b=0, c=1):
2...     print(a, b, c)
3...
4...
5>>> foo(...)
6Ellipsis 0 1

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

... 在 Type Hints 中的使用

不定长的 Tuple, 可用 Ellipsis 指定
1from typing import Tuple
2
3def foo() -> Tuple[int, int]:
4    return (1, 2, 3)

上面代码用 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, 这时就得用 ..., 写成
1from typing import Tuple
2
3def foo() -> Tuple[int, ...]:   # 表示不定长,但类型全为 int
4    return (1, 2, 3)

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 类型提示时第一个参数必须是 ..., 假如写下面的代码
1from typing import Callable
2
3def foo() -> Callable[int, int]:
4    return lambda x: 1

自己设想 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] 的第一个位置上还必须为 ..., 想明确也不行,所以应该写成
1from typing import Callable
2
3def foo() -> Callable[..., int]:
4    return lambda x: 1

这时候你的回调函数写多少个参数都行,如 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
1def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
2      -----------    ----------     ----------
3        |             |                  |
4        |        Positional or keyword   |
5        |                                - Keyword only
6         -- Positional only

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

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

下面代码逐个验证 /* 的功能
 1>>> def standard_arg(arg):
 2...     pass
 3...
 4>>> standard_arg(1)
 5>>> standard_arg(arg=1)
 6>>>
 7>>> def pos_only_arg(arg, /):
 8...     pass
 9...
10>>> pos_only_arg(1)
11>>> pos_only_arg(arg=1)
12Traceback (most recent call last):
13  File "<input>", line 1, in <module>
14    pos_only_arg(arg=1)
15TypeError: pos_only_arg() got some positional-only arguments passed as keyword arguments: 'arg'
16>>>
17>>> def kwd_only_arg(*, arg):
18...     pass
19...
20>>> kwd_only_arg(arg=1)
21>>> kwd_only_arg(1)
22Traceback (most recent call last):
23  File "<input>", line 1, in <module>
24    kwd_only_arg(1)
25TypeError: kwd_only_arg() takes 0 positional arguments but 1 was given
26>>>
27>>> def combined_example(pos_only, /, standard, *, kwd_only):
28...     print(pos_only, standard, kwd_only)
29...
30...
31>>> combined_example(1, 2, kwd_only=3)
321 2 3
33>>> combined_example(1, standard=2, kwd_only=3)
341 2 3
35>>> combined_example(post_only=1, standard=2, kwd_only=3)
36Traceback (most recent call last):
37  File "<input>", line 1, in <module>
38    combined_example(post_only=1, standard=2, kwd_only=3)
39TypeError: combined_example() got an unexpected keyword argument 'post_only'
40>>> combined_example(1, standard=2, 3)
41  File "<bpython-input-141>", line 1
42    combined_example(1, standard=2, 3)
43                                     ^
44SyntaxError: positional argument follows keyword argument

*args, **kwargs 函数参数

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

*** 可用于拆解参数,如
1>>> def foo(a, b, c):
2...     print(a, b, c)
3...
4...
5>>> foo(*[2, 3, 1])   # 把列表拆了依次按位置对应参数
62 3 1
7>>> foo(**{"a":2, "b":3, "c": 1}) # 把字典拆了按关键字对应参数
82 3 1

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

  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
1>>> def foo(*args):
2...     print(type(args))
3...     print(args)
4...
5...
6>>> foo(2,3,1)
7<class 'tuple'>
8(2 3 1)

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

单个值的 2,3,1 => *args, 而不是 args
类似的 **kwargs 就是不定数量的按关键字传递的参数
1>>> def foo(**kwargs):
2...     print(type(kwargs))
3...     print(kwargs)
4...
5...
6>>> foo(a=2, c=3, b=1)
7<class 'dict'>
8{'a': 2, 'c': 3, 'b': 1}

同样的理解方式,把 **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 任意一种方式都可以帮我创建无限行为的函数, 通中会结合二者来向一个未知函数传递所有的参数
 1>>> def foo(fn, *args, **kwargs):
 2...     fn(*args, **kwargs)
 3...
 4...
 5>>> def bar(a, b, *, c):
 6...     print(a, b, c)
 7...
 8...
 9>>> foo(bar, 1, 3, c=4)
101 3 4

再一个较典型的例子就是装饰器中,因为装饰器需要由外部的装饰器函数调用另一个被装饰的函数,而被修饰的函数的参数是不定的
 1def my_decorator(func):
 2    def wrapper(*args, **kwargs):
 3        print(f"before calling {func.__name__}")
 4        func(*args, **kwargs)
 5        print(f"after calling {func.__name__}")
 6
 7    return wrapper
 8
 9
10@my_decorator
11def say_hello(firstname, lastname, **kwargs):
12    print(f"Hello {firstname} {lastname}!", kwargs)
13
14
15say_hello("Steve", "Jobs", company="Apple", country="USA")

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

链接:

  1. Python 的 Ellipsis 对象
永久链接 https://yanbin.blog/python-ellipsis-star-slash-args-kwargs/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。