K 的一隅

Python Python 语言核心

参数怎么传、函数当值用:Python 函数语法

从 def 定义到位置/关键字/默认参数,再到 *args 与 **kwargs 收拢多余实参,以及 return 与函数作为一等值传递。

9 分钟阅读 更新于 2026-07-28
本文目录

把下面这段代码直接跑一遍,再往下看解释——很多参数规则,看一次调用栈比背表格快:

python
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 在定义时创建一个函数对象,并把它绑到当前命名空间里的名字;函数体在调用时才执行

python
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 会立刻结束本次调用:

python
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),不是两个独立返回值:

python
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,顺序可以打乱(必填项必须都出现):

python
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+ 还可以用 / 标记仅限位置的参数(调用时不能写参数名):

python
def send(raw: bytes, /, encoding: str = "utf-8") -> str:
    return raw.decode(encoding)

这里 raw 只能按位置传;encoding 可以位置也可以关键字。日常代码里 / 不如 * 常见,知道有这层语法即可。

默认参数:定义时绑定一次

默认值在函数定义时求值一次,不是每次调用时重新计算:

python
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 完全可以:

python
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

python
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'}

调用时也可以反向解包:序列前的 *、映射前的 ** 把实参展开:

python
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 原样转发,后面装饰器一章会再遇到这个模式:

python
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 接住全部实参,再原样传给 fnreturn fn(...) 把内层函数的返回值(或异常)交还给调用方——这就是装饰器替换函数名时仍能“透明”转发调用的原因。

函数是一等公民:可以当值传来传去

函数对象和整数、列表一样,可以赋给变量、放进容器、当作实参或返回值:

python
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

内置的 mapfilter 也接受 callable。下面用 def 写 key 函数,语义与 lambda 相同,只是多了名字:

python
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 配置):

python
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。标注可以逐步补,复杂签名配合 TypedDictProtocol 等留到类型标注专章。

文档字符串:给调用者看的说明

函数体第一行若是字符串字面量,会成为 __doc__,供 help() 与 IDE 提示使用:

python
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 里写参数含义、返回值、可能抛出的异常——风格因团队而异,保持简短即可。

实参与形参:一次匹配的先后顺序

理解一次调用,可以按下面顺序在脑子里过一遍:

  1. 位置实参从左到右填形参,直到位置实参用完或遇到 *args
  2. 关键字实参按名字填剩余形参
  3. 仍有多余位置实参 → 进入 *args(若存在)
  4. 仍有多余关键字实参 → 进入 **kwargs(若存在)
  5. 必填形参若仍未绑定 → TypeError

缺少 * 分隔时,带默认值的形参也可以纯用位置传;加了 bare * 之后,其右侧形参只能关键字传——这是刻意限制调用写法,避免“第几个位置参数是啥”这类歧义。库作者在设计 API 时常用 * 把“很少改的配置项”推到关键字区。

小实验:逐段猜输出

python
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 → 形参 a
  • 3 → 第二个位置实参,给 b(不是给 *args
  • 4 → 多出来的位置实参,进入 *args
  • c=5 → 仅限关键字形参 c
  • d=6 → 不在形参列表里的关键字,进入 **kw

再试一个 return 与默认值的组合:

python
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

python
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 解包把多个序列拼成一次调用,在拼接命令行参数、合并配置项时很常见:

python
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 在哪一层找到它。