KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA
02 · 函数:参数规则、LEGB 与一等函数 — keel 龙骨
## 这一章解决什么问题
这一章解决什么问题
函数人人会写,但三类问题反复出现:参数传错(*args/**kwargs 分不清)、变量改不动(作用域规则没吃透)、以及不理解「为什么函数能当参数传」。这一章把三件事一次讲透。一等函数是后续课闭包、装饰器、回调注册的地基,是全课最重要的概念。
参数的四种形态
def demo(a, b, /, c, *, d, e): # 仅位置 | 任意 | 仅关键字
print(a, b, c, d, e)
demo(1, 2, 3, d=4, e=5) # ✅
demo(1, 2, c=3, d=4, e=5) # ✅ c 在 / 之后可以写关键字
demo(a=1, b=2, c=3, d=4, e=5) # ❌ a、b 在 / 之前,只能按位置传
demo(1, 2, 3, 4, 5) # ❌ d、e 在 * 之后,只能按关键字传
/ 之前的参数仅限位置,* 之后的参数仅限关键字。日常代码里你很少需要写 /,但你要读懂标准库里的它——len(obj, /) 的意思是「参数名不是对外承诺,别用关键字传」。
*args 和 **kwargs 是收集多余参数的「打包」:
def record(event, *fields, **metadata):
print(event, fields, metadata)
record("click", "btn", "left", page=3, ts=123)
# click ('btn', 'left') {'page': 3, 'ts': 123}
反过来,调用处的 * 和 ** 是「解包」——把序列/字典摊开成参数。这两个方向经常一起看晕,记牢:定义处是打包,调用处是解包。
args = (1, 2)
opts = {"d": 4, "e": 5}
demo(*args, 3, **opts) # 等价于 demo(1, 2, 3, d=4, e=5)
*args 之后声明的参数变成 keyword-only——这是标准库里「强制调用方写关键字」的常用手段,因为 def connect(host, *, port=6379, db=0) 里写 port= 一眼比 connect("h", 6379) 可读。
LEGB:变量查找的四层顺序
Python 找一个名字,按 L → E → G → B 的顺序查,查到即停:
- Local:当前函数内部
- Enclosing:外层函数(闭包涉及的那层)
- Global:模块顶层
- Built-in:Python 内置(print、len……)
x = "global"
def outer():
x = "enclosing"
def inner():
x = "local"
print(x) # local
print(len) # built-in:local/enclosing/global 都没有 len
inner()
outer()
两个必须知道的坑:
坑一:读不需要声明,写需要。 函数内给名字赋值,Python 就把它当局部变量——哪怕外层有同名变量:
count = 0
def bump():
count = count + 1 # ❌ UnboundLocalError
# 右边的 count 被判定为"局部变量",但还没赋值就要读它
在函数里重新绑定外层变量,需要 global(对应模块层)或 nonlocal(对应外层函数层):
def bump():
global count
count = count + 1 # ✅
工程建议:global 能不用就不用——它让函数产生模块级的副作用,测试很难做。计数这种状态,用类或者显式传参更干净。
坑二:默认参数求值时机(上一章讲过,这里是作用域视角)——def 那一刻的名字查找发生在定义处,不在调用处。
一等函数:函数就是对象
Python 里函数和 int、list 没有本质区别——都是对象,都能:
def shout(s): return s.upper()
def quiet(s): return s.lower()
# 1. 赋值给变量
speak = shout
# 2. 存进数据结构
actions = {"shout": shout, "quiet": quiet}
# 3. 当参数传、当返回值返回
def apply(fn, s): return fn(s)
# 4. 运行时查询自己的信息
shout.__name__ # 'shout'
这直接解锁了「用 dict 替代 if/elif 分发」的写法:
def handle_create(payload): ...
def handle_delete(payload): ...
def handle_query(payload): ...
handlers = {"create": handle_create, "delete": handle_delete, "query": handle_query}
def dispatch(action, payload):
handler = handlers.get(action)
if handler is None:
raise ValueError(f"unknown action: {action}")
return handler(payload)
后续课里的「命令注册表」「按事件类型分发 handler」,形状完全一样,只是 handler 里多了注册环节(handlers[event_type] = fn)。
sorted 的 key 参数是「函数当参数」最日常的用法:
users = [{"name": "cb", "age": 30}, {"name": "ad", "age": 25}]
sorted(users, key=lambda u: u["age"])
max(records, key=lambda r: r.get("score", 0))
lambda 只适合一行的表达式。超过一行就写 def,别硬塞。
一等函数 + 嵌套定义 = 闭包的前奏
函数里可以定义函数,内层函数可以引用外层的变量——这两个事实合起来,就是闭包:
def make_greeter(prefix):
def greet(name):
return f"{prefix}, {name}" # prefix 来自外层函数
return greet # 返回内层函数
hello = make_greeter("hello")
hi = make_greeter("hi")
hello("bob") # 'hello, bob'
hi("bob") # 'hi, bob' —— 两次调用各有一份自己的 prefix
注意 make_greeter 已经返回了,但 prefix 还活着——内层函数把外层变量「关」在了自己身体里。这就是闭包。03 章用它造装饰器,这是全课最漂亮的一次概念接力。
自检三题
def f(x, *, strict=False)里*单独出现是什么意思?UnboundLocalError和NameError的区别是什么?什么操作会触发前者?- 把
if/elif五个分支改成 dict 分发,写出关键三行。
下一章
03 章把「嵌套函数 + 返回函数」推进到装饰器—— Python 里声明式写法(鉴权、重试、缓存)的全部秘密。