前言

函数是 Python 的乐高积木。参数传递的几种形态、作用域查找规则、闭包——这三块搞明白,写函数式风格的代码就通了。

一、参数的四种形态

def connect(host, port=5432, *, timeout=3, **extra):
    print(f"{host}:{port} timeout={timeout} extra={extra}")

# 1. 位置参数:按顺序传
connect("db.local")

# 2. 关键字参数:指名道姓传(推荐, 可读性好)
connect(host="db.local", port=5433)

# 3. 仅关键字参数(* 之后的必须用关键字)
connect("db.local", 5433, timeout=10)

# 4. 可变参数
def add(*args, **kwargs):        # args 收集多余位置参数为 tuple, kwargs 收集关键字为 dict
    print(args, kwargs)

add(1, 2, 3, debug=True)         # (1, 2, 3) {'debug': True}

# 反向解包:调用时把序列/字典摊开
args = ("db.local", 5433)
connect(*args)

⚠️ 默认参数的头号大坑——可变默认值:

def bad(item, lst=[]):           # ❌ 默认列表在函数定义时只创建一次!
    lst.append(item)
    return lst

bad(1)    # [1]
bad(2)    # [1, 2] —— 惊不惊喜?

# 正确姿势
def good(item, lst=None):
    if lst is None:
        lst = []
    lst.append(item)
    return lst

二、作用域:LEGB 规则

Python 查找变量的顺序:Local → Enclosing → Global → Built-in。

x = "global"

def outer():
    x = "enclosing"
    def inner():
        x = "local"
        print(x)          # local(L 优先)
    inner()

outer()

想在函数内修改外层变量,必须声明:

count = 0

def bump():
    global count          # 改全局需要 global
    count += 1

def make_counter():
    n = 0
    def counter():
        nonlocal n        # 改闭包变量需要 nonlocal
        n += 1
        return n
    return counter

良好实践:少用 global——函数应通过参数进、返回值出,保持纯粹。

三、闭包:函数记住它的出生地

闭包 = 内层函数 + 它引用的外层变量。函数返回后,被引用的环境被"记住"了:

def multiplier(factor):
    def multiply(x):
        return x * factor       # 引用了外层的 factor
    return multiply

double = multiplier(2)
triple = multiplier(3)
double(10)      # 20
triple(10)      # 30
print(double.__closure__[0].cell_contents)   # 2 —— factor 被闭包持有

闭包的典型应用是工厂函数和装饰器(装饰器本质就是闭包,后面单独写一篇)。

四、lambda 与高阶函数

# lambda: 一个表达式的小函数
square = lambda x: x * x
key_fn = lambda item: item[1]

# 高阶函数: 接收/返回函数的函数
nums = [3, 1, 4, 1, 5, 9, 2, 6]

list(map(lambda x: x * 2, nums))          # [6, 2, 8, ...]
list(filter(lambda x: x > 3, nums))       # [4, 5, 9, 6]

# sorted 的 key 用法(日常最高频)
words = ["banana", "apple", "cherry"]
sorted(words)                              # 按字母
sorted(words, key=len)                     # 按长度
sorted(words, key=lambda w: w[::-1])       # 按反转拼写(演示而已)

students = [("zy", 88), ("ab", 95), ("cd", 72)]
sorted(students, key=lambda s: s[1], reverse=True)   # 按分数降序

# max/min 也吃 key
max(students, key=lambda s: s[1])          # ('ab', 95)

其实大多数 map/filter 场景,推导式可读性更好:

[x * 2 for x in nums]
[x for x in nums if x > 3]

五、实战:一个通用重试函数

把"参数默认值 + 闭包 + 高阶函数"揉在一起:

import time
from functools import wraps

def retry(times=3, delay=1):
    """装饰器工厂: 生成一个重试装饰器"""
    def decorator(func):
        @wraps(func)                        # 保留原函数元信息
        def wrapper(*args, **kwargs):
            for attempt in range(1, times + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    print(f"第{attempt}次失败: {e}")
                    if attempt == times:
                        raise               # 最后一次失败就抛出
                    time.sleep(delay)
        return wrapper
    return decorator

@retry(times=5, delay=2)
def fetch(url):
    import urllib.request
    return urllib.request.urlopen(url, timeout=3).status

这段代码先用着,原理在《装饰器与生成器》一篇展开。

六、函数是一等公民

# 函数可以存进数据结构、当参数传、当返回值
ops = {"add": lambda a, b: a + b, "sub": lambda a, b: a - b}
ops["add"](3, 4)               # 7

def apply(f, data):
    return [f(x) for x in data]

apply(str.upper, ["go", "py"])  # ['GO', 'PY']

小结

知识点 要点
可变默认参数 用 None 哨兵值
作用域 LEGB;改外层用 global/nonlocal
闭包 内层函数持有外层变量
lambda 配 sorted/map/filter 的 key 用
高阶函数 函数可以当值传递

本文是「Python」系列第 3 篇。