函数是 Python 世界里的第一等公民。掌握它,就掌握了把复杂度关进笼子里的钥匙。
一、为什么需要函数
-
复用:写一次,用 N 次。
-
抽象:把"怎么做"隐藏,只暴露"做什么"。
-
测试:最小可测单元,方便单元测试。
-
组合:乐高式地拼出更大的功能。
二、函数的本质:可调用的对象
在 Python 里,一切皆对象,函数也不例外。
python
>>> def add(a, b): return a + b
>>> type(add)
<class 'function'>
>>> add.__call__(3, 4) # 函数对象内部实现了 __call__
7
因此函数可以:
-
赋值给变量
-
放进列表 / 字典
-
作为参数或返回值
三、定义函数:语法全景图
python
def 函数名([参数列表]) -> 返回注解:
"""可选的文档字符串"""
函数体
[return 表达式]
-
函数名 :遵循蛇形命名
snake_case
,动词优先。 -
冒号 + 缩进 代替 C/Java 的花括号。
-
return 可省略,此时隐式返回
None
。 -
注解 只是元数据,可被
__annotations__
查看,不做运行时检查。
四:参数类型全景
1.位置参数(Positional-only Arguments,最"老实"的参数)
这是最传统、最没有花样的参数形态:你在定义时写一个名字,调用时按先后顺序把值塞进去即可。它既不能跳过,也不能用关键字"点名"。例如 def power(base, exponent)
,调用时必须写 power(2, 3)
,不能写 power(base=2, exponent=3)
------顺序一旦写错,结果就南辕北辙。位置参数的好处是直观、高效;坏处是当参数一多,可读性会骤降,因此 Python 3.8 引入了"仅限位置参数"的语法符号 /
,把这一风格显式化:
python
def tag(name, content, /):
return f"<{name}>{content}</{name}>"
2.默认值参数(Default Argument Values,给参数一个"备胎")
有时某个参数在大多数场景里都是一个固定值,为了避免每次调用都重复书写,可以给它一个默认值。语法是在形参后加 =
和一个表达式。注意:表达式只在函数定义时求值一次,如果默认值是可变对象(列表、字典、集合),就可能踩到"共享陷阱"。典型写法:
python
def connect(host, port=3306, timeout=5):
...
调用时可以省略已有默认值的参数:connect('db.example.com')
、connect('db.example.com', 5432)
。默认值参数必须放在位置参数之后,否则解释器无法分辨谁是谁。
3.可变位置参数(Var-Positional,*args)
当你不确定调用者会传多少个位置参数,或者想让它"想传几个就传几个"时,可以在形参名前加一个星号 *
。Python 会把所有多余的位置实参打包成一个元组,绑到这个形参上。函数内部可以像遍历普通元组一样遍历 args
。
python
def mean(*values):
return sum(values) / len(values) if values else 0.0
调用示例:mean(1, 2, 3, 4)
、mean()
都合法。它带来的好处是接口极致简洁,坏处是静态类型检查工具(如 mypy)无法推断每个元素的具体类型,需要额外标注 Tuple[int, ...]
。
4.可变关键字参数(Var-Keyword,**kwargs)
与 *args
类似,但收集的是"关键字实参"。在形参名前加两个星号 **
,Python 会把所有未匹配的关键字实参打包成一个字典。典型场景是"透传"参数,如写装饰器时把上层收到的不确定关键字参数全部交给被装饰函数:
python
def trace(func, *args, **kwargs):
print(f"Calling {func.__name__} with {args} and {kwargs}")
return func(*args, **kwargs)
调用示例:trace(open, 'file.txt', mode='wb', buffering=0)
。字典键都是字符串,值可以是任意对象。
5.仅限关键字参数(Keyword-Only Arguments,用星号"隔出"的参数)
有时候你希望强制调用者用关键字形式传参,以提升可读性或防止顺序写错。做法是在参数列表里放一个裸星号 *
,它之后的所有参数都必须用关键字传递。
python
def create_user(username, *, is_admin=False, send_welcome=True):
...
调用时必须写 create_user('alice', is_admin=True)
,而不能写 create_user('alice', True)
。这一特性在写配置类 API 时尤为有用,能避免"布尔陷阱"。
6.仅限位置参数
与第 5 点相反,/
之前的参数禁止 用关键字形式传递。其目的在于让库作者未来能自由重命名这些参数而不破坏向后兼容。例如 CPython 内置函数 divmod(a, b)
就把两个参数都设为仅限位置,所以我们只能写 divmod(10, 3)
,而不能写 divmod(a=10, b=3)
。
7.解包调用
虽然它发生在调用端而非定义端,但经常与上述参数类型一起出现,因此一并说明。你可以在调用函数时使用 *
把序列/可迭代对象解包成位置实参,用 **
把字典解包成关键字实参。
python
coords = (4, 5)
point = {'x': 10, 'y': 20}
move_to(*coords) # 等价于 move_to(4, 5)
create_point(**point) # 等价于 create_point(x=10, y=20)
当序列长度与形参数量不匹配时会抛出 TypeError
,需要保证一一对应。
五、参数传递机制:共享传参(Call by Object Reference)
-
Python 没有真正的"值传递"或"引用传递"。
-
形参 = 实参对象的 新引用。
-
若对象可变,原地修改 对外部可见;若不可变,重新绑定不影响外部。
python
def append_one(xs):
xs.append(1) # 列表可变,外部可见
def set_zero(x):
x = 0 # 整数不可变,外部无感
lst = []
append_one(lst); print(lst) # [1]
n = 42
set_zero(n); print(n) # 42
六、高级话题速览
-
高阶函数
map / filter / functools.reduce / sorted(key=...)
-
闭包与延迟绑定陷阱
python
fs = [lambda: i for i in range(3)]
print([f() for f in fs]) # [2, 2, 2]
# 修复:lambda i=i: i
-
装饰器
本质是"函数返回函数",加上
@
语法糖。 -
单分派泛函数
@functools.singledispatch
实现 C++ 风格重载。 -
类型提示与静态检查
python
from typing import List, Callable
def pipeline(data: List[int], fn: Callable[[int], int]) -> List[int]: ...
七、最佳实践清单
-
单一职责:一个函数只做一件事。
-
早返回(return early)减少嵌套。
-
使用
dataclasses
或NamedTuple
替代裸字典返回。 -
文档字符串格式:
"""Do X and return Y."""
或 Google / NumPy 风格。 -
对可变默认值使用
None
占位:
python
def append(item, seq=None):
if seq is None:
seq = []
八、小结
函数是 Python 抽象、组合、复用 的核心。
从最简单的 def
到装饰器、闭包、类型提示,每一步都让代码更声明式 、更可测试。