本文目录
把下面这段代码直接跑一遍,再往下看解释——很多参数规则,看一次调用栈比背表格快:
def greet(name: str, prefix: str = "Hi", *, punct: str = "!") -> str:
return f"{prefix}, {name}{punct}"
print(greet("Ada"))
print(greet("Ada", prefix="Hello"))
print(greet("Ada", punct="."))第一行用默认的 prefix="Hi" 和 punct="!";第二行用关键字改 prefix;第三行只改 punct。注意单独的 * 表示其后是仅限关键字的参数:不能写 greet("Ada", "!"),必须 punct="!"。
上一篇讲容器与赋值;从这一篇起,逻辑块被 def 包成可反复调用的对象。参数怎么匹配、多出来的实参去哪、return 交还什么,是后面类方法、装饰器、lambda 的共同地基。
和其他语言相比,Python 函数最“显式”的地方在于参数列表本身携带大量信息:默认值、收拢多余实参的星号、强制关键字传的 bare *。把这些规则当成调用协议的一部分,而不是死记语法,后面读标准库签名会轻松很多。
def:定义函数,调用才执行
def 在定义时创建一个函数对象,并把它绑到当前命名空间里的名字;函数体在调用时才执行。
def double(x: int) -> int:
print(f"double 收到 {x}")
return x * 2
y = double(3) # 打印 double 收到 3
print(y) # 6定义阶段不会打印 double 收到 ...——只有 double(3) 才真正跑函数体。同一个 def 只执行一次;之后每次调用走各自的局部命名空间。
return:交还结果,或提前结束
没有 return 时,函数执行到末尾会隐式 return None。显式 return 会立刻结束本次调用:
def find_first_even(nums: list[int]) -> int | None:
for n in nums:
if n % 2 == 0:
return n
return None
print(find_first_even([1, 3, 5])) # None
print(find_first_even([1, 4, 3])) # 4写 return a, b 时,实际返回一个元组 (a, b),不是两个独立返回值:
def min_max(values: list[float]) -> tuple[float, float]:
return min(values), max(values)
lo, hi = min_max([1.5, 0.2, 3.1])
print(lo, hi) # 0.2 3.1位置参数与关键字参数
位置参数按形参从左到右匹配;关键字参数用 name=value,顺序可以打乱(必填项必须都出现):
def connect(host: str, port: int, timeout: float = 5.0) -> str:
return f"{host}:{port} t={timeout}"
print(connect("127.0.0.1", 8080))
print(connect(port=443, host="example.com", timeout=2.0))混用时,所有位置实参必须出现在第一个关键字实参之前:connect(443, host="x") 合法,connect(host="x", 443) 不合法。
Python 3.8+ 还可以用 / 标记仅限位置的参数(调用时不能写参数名):
def send(raw: bytes, /, encoding: str = "utf-8") -> str:
return raw.decode(encoding)这里 raw 只能按位置传;encoding 可以位置也可以关键字。日常代码里 / 不如 * 常见,知道有这层语法即可。
默认参数:定义时绑定一次
默认值在函数定义时求值一次,不是每次调用时重新计算:
def append_to(item: int, bucket: list[int] | None = None) -> list[int]:
if bucket is None:
bucket = []
bucket.append(item)
return bucket
print(append_to(1)) # [1]
print(append_to(2)) # [2] — 不会变成 [1, 2]若写成 bucket: list[int] = [],默认值对象在定义时创建一次,多次调用共享同一个列表——新手高频坑。可变默认值用 None 占位,在函数体内按需新建。
*args 与 **kwargs:收拢“多出来的”实参
形参列表里的 *args 把多出来的位置实参收成元组;**kwargs 把多出来的关键字实参收成字典。名字只是约定,叫 *parts、**options 完全可以:
def log(level: str, *args: object, **kwargs: object) -> None:
print(level, args, kwargs)
log("INFO", "boot", "ok", user="ada", retries=2)
# INFO ('boot', 'ok') {'user': 'ada', 'retries': 2}更完整的形参排列往往是:必填位置 → 带默认值 → *args → 仅限关键字 → **kwargs:
def mix(a: int, b: int = 0, *args: int, flag: bool = False, **opts: str) -> None:
print(a, b, args, flag, opts)
mix(1, 2, 3, 4, flag=True, encoding="utf-8")
# 1 2 (3, 4) True {'encoding': 'utf-8'}调用时也可以反向解包:序列前的 *、映射前的 ** 把实参展开:
def add(a: int, b: int, c: int = 0) -> int:
return a + b + c
nums: tuple[int, ...] = (10, 20)
extra: dict[str, int] = {"c": 5}
print(add(*nums, **extra)) # 35,等价 add(10, 20, c=5)包装函数时经常用 *args, **kwargs 原样转发,后面装饰器一章会再遇到这个模式:
def trace(fn: callable[..., object]) -> callable[..., object]:
def wrapper(*args: object, **kwargs: object) -> object:
print(f"call {fn.__name__}")
return fn(*args, **kwargs)
return wrapper外层 wrapper 不知道内层函数具体有几个参数,于是用 *args, **kwargs 接住全部实参,再原样传给 fn。return fn(...) 把内层函数的返回值(或异常)交还给调用方——这就是装饰器替换函数名时仍能“透明”转发调用的原因。
函数是一等公民:可以当值传来传去
函数对象和整数、列表一样,可以赋给变量、放进容器、当作实参或返回值:
def square(n: int) -> int:
return n * n
def apply(fn: callable[[int], int], value: int) -> int:
return fn(value)
print(apply(square, 4)) # 16
ops: dict[str, callable[[int], int]] = {"sq": square}
print(ops["sq"](5)) # 25内置的 map、filter 也接受 callable。下面用 def 写 key 函数,语义与 lambda 相同,只是多了名字:
def by_length(word: str) -> int:
return len(word)
words: list[str] = ["fig", "banana", "apple"]
print(sorted(words, key=by_length))高阶函数(接受或返回函数的函数)在排序、回调、事件处理里很常见。核心就一点:函数名只是指向函数对象的引用,传 square 时不带括号,传的是对象本身。
类型标注:写给人和工具,不改变运行时
参数后的 : Type、返回值前的 -> Type 是标注;CPython 运行时基本不检查(除非用了 @typing 运行时工具或特殊 linter 配置):
def clamp(value: float, low: float = 0.0, high: float = 1.0) -> float:
return max(low, min(high, value))*args: int 表示“这一组额外位置实参期望是 int”;**kwargs: str 表示关键字值期望是 str。标注可以逐步补,复杂签名配合 TypedDict、Protocol 等留到类型标注专章。
文档字符串:给调用者看的说明
函数体第一行若是字符串字面量,会成为 __doc__,供 help() 与 IDE 提示使用:
def pct(part: float, whole: float) -> float:
"""返回 part 占 whole 的百分比(0–100)。"""
if whole == 0:
return 0.0
return part / whole * 100
print(pct.__doc__)文档字符串不改变调用行为,但长期维护时,比散落在注释里的说明更容易被工具发现。复杂函数可以在 docstring 里写参数含义、返回值、可能抛出的异常——风格因团队而异,保持简短即可。
实参与形参:一次匹配的先后顺序
理解一次调用,可以按下面顺序在脑子里过一遍:
- 位置实参从左到右填形参,直到位置实参用完或遇到
*args - 关键字实参按名字填剩余形参
- 仍有多余位置实参 → 进入
*args(若存在) - 仍有多余关键字实参 → 进入
**kwargs(若存在) - 必填形参若仍未绑定 →
TypeError
缺少 * 分隔时,带默认值的形参也可以纯用位置传;加了 bare * 之后,其右侧形参只能关键字传——这是刻意限制调用写法,避免“第几个位置参数是啥”这类歧义。库作者在设计 API 时常用 * 把“很少改的配置项”推到关键字区。
小实验:逐段猜输出
def f(a: int, b: int = 2, *args: int, c: int, **kw: int) -> None:
print(a, b, args, c, kw)
f(1, 3, 4, c=5, d=6)输出:1 3 (4,) 5 {'d': 6}。
1→ 形参a3→ 第二个位置实参,给b(不是给*args)4→ 多出来的位置实参,进入*argsc=5→ 仅限关键字形参cd=6→ 不在形参列表里的关键字,进入**kw
再试一个 return 与默认值的组合:
def status(code: int, msg: str = "OK") -> dict[str, int | str]:
return {"code": code, "msg": msg}
print(status(404, msg="Not Found"))
print(status(500))第二行 msg 用默认值 "OK";返回值是字典,不是两个独立对象。
再观察 *args 为空时的形状——没有多余位置实参时,它是空元组 (),不是 None:
def show(*parts: str) -> None:
print(type(parts), parts)
show() # <class 'tuple'> ()
show("a", "b") # <class 'tuple'> ('a', 'b')**kwargs 若无多余关键字,同样是空字典 {}。包装层转发时经常假设“总是 tuple/dict”,调用方不传额外参数也安全。
常见误解
| 误解 | 实际 |
|---|---|
*args 必须是 tuple 类型名 | 只是约定俗成的名字,叫 *rest 也行 |
| 有默认值的参数都能用位置传 | 单独 * 之后的形参只能用关键字传 |
return 只能返回一个对象 | return a, b 返回一个元组 |
| 标注错了运行一定报错 | 默认情况下运行时忽略标注 |
调用 f 和引用 f 是一回事 | f() 是调用;g = f 是绑定同一个函数对象 |
与容器、作用域的衔接
函数参数传递的是对象的引用(上一篇容器那一套):把 list 传给函数,形参名指向同一对象,在函数内原地 append 会影响调用方的列表。若希望不共享,应在函数内新建容器或让调用方传副本。
函数体里新赋值的名字默认是局部(下一篇 LEGB 的 L 层);读外层模块的全局常量可以,但在函数里 count = count + 1 若不写 global,会踩 UnboundLocalError。先会写 def 和参数,再读作用域,顺序是刻意的:调用协议与名字查找是两件正交的事。
再补一个与容器相关的习惯用法:用 *seq 解包把多个序列拼成一次调用,在拼接命令行参数、合并配置项时很常见:
def run(*argv: str) -> None:
print(" ".join(argv))
base: tuple[str, ...] = ("python", "-m", "pip")
extra: tuple[str, ...] = ("install", "httpx")
run(*base, *extra)这里两次 * 解包把两个 tuple 展平成四个位置实参。理解“定义处的 *args 收拢”与“调用处的 * 展开”成对出现,读第三方库源码时会轻松很多。
小结
函数是 Python 里最常用的抽象单元:def 创建对象,调用时绑定实参与形参,return 把结果交还给调用方。*args / **kwargs 处理“多出来的”实参;bare * 限制某些参数必须按关键字传。函数名只是引用,可以像其他对象一样传递——这为 lambda、装饰器和高阶 API 打下基础。日常写库 API 时,优先让必填项靠前、默认值和 * 分隔靠后,调用方读起来会少很多 positional 数数的负担。若你维护公共包,在 docstring 里写清哪些参数只能关键字传,能减少 issue 里“为什么传 positional 报错”的重复提问。下一篇不改参数列表,只回答:函数体里的名字,解释器按 LEGB 在哪一层找到它。