早先对 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 类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
def Form( # noqa: N802 default: Any, *, media_type: str = "application/x-www-form-urlencoded", alias: Optional[str] = None, title: Optional[str] = None, description: Optional[str] = None, gt: Optional[float] = None, ge: Optional[float] = None, lt: Optional[float] = None, le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, regex: Optional[str] = None, example: Any = Undefined, examples: Optional[Dict[str, Any]] = None, **extra: Any, ) -> Any: |
Form(...)
应该是把 ...
传给了第一个参数 default: Any
. 验证一下
1 2 3 4 5 6 |
>>> def foo(a, *, b=0, c=1): ... print(a, b, c) ... ... >>> foo(...) Ellipsis 0 1 |
这样看 ...
没什么特别的,纯粹就是一个 Python 常量。
...
在 Type Hints 中的使用
不定长的 Tuple, 可用 Ellipsis 指定
1 2 3 4 |
from typing import Tuple def foo() -> Tuple[int, int]: 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, 这时就得用 ...
, 写成
1 2 3 4 |
from typing import Tuple def foo() -> Tuple[int, ...]: # 表示不定长,但类型全为 int 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 类型提示时第一个参数必须是 ...
, 假如写下面的代码
1 2 3 4 |
from typing import Callable def foo() -> Callable[int, int]: 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] 的第一个位置上还必须为 ...
, 想明确也不行,所以应该写成
1 2 3 4 |
from typing import Callable def foo() -> Callable[..., int]: 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
1 2 3 4 5 6 |
def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2): ----------- ---------- ---------- | | | | Positional or keyword | | - Keyword only -- Positional only |
简单的规则就是: /
前只能按位置传递 (Positional-Only), *
后只能按关键字传递 (Keyword-Only),/
后 和 *
前(中间)的随意。因为 /
管前面,*
管后面,所以一个函数中同时有 /
和 *
时,/
必须写成 *
前面。
在 FastAPI 框架中声明函数常用 *
, 它的一个约定是对于必需的无默认值的参数放在 *
前面,可用位置或关键字来传递,相应的有默认值的参数放在 *
后面作为可选参数,我们也可以借鉴这种用法。
下面代码逐个验证 /
和 *
的功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
>>> def standard_arg(arg): ... pass ... >>> standard_arg(1) >>> standard_arg(arg=1) >>> >>> def pos_only_arg(arg, /): ... pass ... >>> pos_only_arg(1) >>> pos_only_arg(arg=1) Traceback (most recent call last): File "<input>", line 1, in <module> pos_only_arg(arg=1) TypeError: pos_only_arg() got some positional-only arguments passed as keyword arguments: 'arg' >>> >>> def kwd_only_arg(*, arg): ... pass ... >>> kwd_only_arg(arg=1) >>> kwd_only_arg(1) Traceback (most recent call last): File "<input>", line 1, in <module> kwd_only_arg(1) TypeError: kwd_only_arg() takes 0 positional arguments but 1 was given >>> >>> def combined_example(pos_only, /, standard, *, kwd_only): ... print(pos_only, standard, kwd_only) ... ... >>> combined_example(1, 2, kwd_only=3) 1 2 3 >>> combined_example(1, standard=2, kwd_only=3) 1 2 3 >>> combined_example(post_only=1, standard=2, kwd_only=3) Traceback (most recent call last): File "<input>", line 1, in <module> combined_example(post_only=1, standard=2, kwd_only=3) TypeError: combined_example() got an unexpected keyword argument 'post_only' >>> combined_example(1, standard=2, 3) File "<bpython-input-141>", line 1 combined_example(1, standard=2, 3) ^ SyntaxError: positional argument follows keyword argument |
*args, **kwargs 函数参数
首先 args
和 kwargs
是两个参数命名的习俗,按自己的喜爱可以用任意的词,如 *aaa
, **bbb
,不过最好是按照大家的习惯来命名。
*
和 **
可用于拆解参数,如
1 2 3 4 5 6 7 8 |
>>> def foo(a, b, c): ... print(a, b, c) ... ... >>> foo(*[2, 3, 1]) # 把列表拆了依次按位置对应参数 2 3 1 >>> foo(**{"a":2, "b":3, "c": 1}) # 把字典拆了按关键字对应参数 2 3 1 |
思考并复习上一节的内容:
- 如果定义 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
1 2 3 4 5 6 7 8 |
>>> def foo(*args): ... print(type(args)) ... print(args) ... ... >>> foo(2,3,1) <class 'tuple'> (2 3 1) |
args 是一个列表,怎么去理解这一个调用过程呢?传入单个值的,在函数中变成了一个列表,看着想是 *
拆解的逆过程。可以这么理解,把 *args
整体看作一个参数,那么可以想像该参数 *args
是一个已拆解的列表,那么去掉 *
号的 args
就是列表本身。所以传参时是这么对应的
单个值的
2,3,1
=>*args
, 而不是args
类似的 **kwargs
就是不定数量的按关键字传递的参数
1 2 3 4 5 6 7 8 |
>>> def foo(**kwargs): ... print(type(kwargs)) ... print(kwargs) ... ... >>> foo(a=2, c=3, b=1) <class 'dict'> {'a': 2, 'c': 3, 'b': 1} |
同样的理解方式,把 **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
任意一种方式都可以帮我创建无限行为的函数, 通中会结合二者来向一个未知函数传递所有的参数
1 2 3 4 5 6 7 8 9 10 |
>>> def foo(fn, *args, **kwargs): ... fn(*args, **kwargs) ... ... >>> def bar(a, b, *, c): ... print(a, b, c) ... ... >>> foo(bar, 1, 3, c=4) 1 3 4 |
再一个较典型的例子就是装饰器中,因为装饰器需要由外部的装饰器函数调用另一个被装饰的函数,而被修饰的函数的参数是不定的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
def my_decorator(func): def wrapper(*args, **kwargs): print(f"before calling {func.__name__}") func(*args, **kwargs) print(f"after calling {func.__name__}") return wrapper @my_decorator def say_hello(firstname, lastname, **kwargs): print(f"Hello {firstname} {lastname}!", kwargs) say_hello("Steve", "Jobs", company="Apple", country="USA") |
这是摘自我之前 熟悉和应用 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 […]