0. 为什么先学函数
在 Python 的学习曲线里,函数是第一个真正意义上的“抽象”工具。语法 if/for 只是流程控制,而函数让我们把“做什么”与“怎么做”分离。掌握了函数,你就能:
• 把 100 行脚本拆成 10 个可测试、可组合的小部件;
• 通过参数化让同一段逻辑服务多种场景;
• 用装饰器、生成器等高级特性写出优雅 DSL;
• 在阅读源码时快速定位作者意图:函数名就是注释。
1.函数的本质:从“代码片段”到“可复用抽象”
计算机科学大师 Harold Abelson 说:“程序设计语言并不仅仅是告诉计算机做什么,更是组织抽象的手段。”
在 Python 中,函数是一类对象,它具备:
-
identity(唯一标识,id(func))
-
state(defaults、closure、dict)
-
behavior(call)
因此,函数比“可复用的代码片段”多出两层含义:
(1) 它是数据,可存列表、当键值、动态生成;
(2) 它是契约,通过签名(signature)对外承诺输入与输出。
2. 语法速写:定义、参数、返回值
最简形式:
def add(a, b=1):
"""Return sum of a and b."""
return a + b
拆解:
-
def 关键字创建函数对象,绑定到当前作用域名字 add;
-
参数分两类:位置参数 a,默认参数 b;
-
函数体在编译时变成代码对象 code object;
-
return 把结果压栈并终止函数,无 return 隐式 return None。
3. 参数传递全景图
3.1 位置与关键字
位置参数按顺序赋值;关键字参数通过名字赋值,可增加可读性:
def tag(name, /, content, *, cls=None, **attrs):
...
/ 前为仅位置,* 后为仅关键字(Python 3.8+)。
3.2 可变参数
def mean(first, *rest, ignore_nan=False):
data = (first,) + rest
if ignore_nan:
data = [x for x in data if x == x] # 过滤 NaN
return sum(data) / len(data)
*args 打包成 tuple,**kwargs 打包成 dict。
3.3 解包调用
args = (1, 2, 3)
config = {'ignore_nan': True}
mean(*args, **config)
3.4 只读默认参数陷阱
默认参数在 def 语句执行时求值一次:
def append(x, lst=[]): # 错误示范
lst.append(x)
return lst
修复:
def append(x, lst=None):
lst = [] if lst is None else lst
lst.append(x)
return lst
4. 作用域与生命周期
4.1 LEGB 规则
Local → Enclosing → Global → Built-in。
x = 1
def outer():
x = 2
def inner():
nonlocal x # 指向 outer 作用域
x += 1
return x
return inner
4.2 闭包
函数对象保存了定义时作用域的引用,形成闭包。典型应用:
def make_power(n):
def power(x):
return x ** n
return power
square = make_power(2)
cube = make_power(3)
4.3 延迟绑定陷阱
在循环中创建闭包:
funcs = [lambda: i for i in range(3)]
print([f() for f in funcs]) # [2, 2, 2]
修复:
funcs = [lambda i=i: i for i in range(3)]
5. 一等公民:把函数当数据用
5.1 高阶函数
filter、sorted、functools.reduce 都接受函数作为参数:
from functools import reduce
reduce(lambda acc, x: acc + [x] if x % 2 else acc, range(10), [])
5.2 函数注册表
registry = {}
def register(name):
def wrapper(fn):
registry[name] = fn
return fn
return wrapper
@register('add')
def _(a, b):
return a + b
5.3 动态派发
def dispatch(op):
return registry[op]
6. 装饰器:不修改源码的“外挂”
6.1 无参装饰器
from functools import wraps
import time
def timethis(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = fn(*args, **kwargs)
print(f"{fn.__name__} took {time.perf_counter() - start:.4f}s")
return result
return wrapper
6.2 带参装饰器
def repeat(n):
def decorator(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
for _ in range(n):
result = fn(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def hello():
print("Hello")
6.3 类装饰器
def singleton(cls):
instances = {}
def getinstance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return getinstance
7. 生成器:用函数实现“状态机”
7.1 惰性迭代
def fib():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
7.2 send & throw
def coro():
print("Start")
x = yield
print("Got", x)
yield x + 1
7.3 yield from 委托
def chain(*iters):
for it in iters:
yield from it
8. 常见误区与最佳实践
-
避免过度嵌套:>3 层考虑拆函数或类;
-
函数长度:一屏(PEP8 建议 79 列 * 50 行)以内;
-
早返回减少缩进;
-
用 docstring 描述“做什么”而非“怎么做”;
-
不要用可变对象为默认参数;
-
限制 *args/**kwargs 使用场景,避免签名过于宽泛。