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 的顺序查,查到即停:

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 章用它造装饰器,这是全课最漂亮的一次概念接力。

自检三题

  1. def f(x, *, strict=False) 里 * 单独出现是什么意思?
  2. UnboundLocalError 和 NameError 的区别是什么?什么操作会触发前者?
  3. 把 if/elif 五个分支改成 dict 分发,写出关键三行。

下一章

03 章把「嵌套函数 + 返回函数」推进到装饰器—— Python 里声明式写法(鉴权、重试、缓存)的全部秘密。

进入 keel 阅读