基础篇 / 第 6 章
第 6 章 · 函数与作用域
为什么要用函数:抽象与复用
在第 3 章我们用循环处理过一列表数据,但当同样的逻辑要在三个地方重复时,复制粘贴就是灾难。函数(function)把「一段有名字、可重复调用、有明确输入输出的代码块」封装起来,让程序从「命令的堆叠」变成「可组合的积木」。
用 def 关键字定义函数,函数体必须缩进。定义本身只是把函数对象绑定到一个名字上,不执行任何代码:
# def_demo.py —— 函数的基本形态
def greet(): # 定义函数:只是绑定名字,不执行
print("你好,欢迎学习 Python!") # 缩进 4 个空格的函数体
print("【调用前】greet 是什么?")
print(greet) # 输出函数对象本身及其内存地址
print("【调用】")
greet() # 第一次调用
greet() # 第二次调用,行为完全一致
# 预期输出:
# 【调用前】greet 是什么?
# <function greet at 0x7f8e1c0b1234> (地址每次运行不同)
# 【调用】
# 你好,欢迎学习 Python!
# 你好,欢迎学习 Python!
| 收益 | 说明 | 示例 |
|---|---|---|
| 复用(reuse) | 逻辑写一次,处处调用 | len()、print() 就是复用函数 |
| 抽象(abstraction) | 隐藏实现细节,只暴露「输入 → 输出」的契约 | 调用者不需要知道 len() 内部如何计数 |
| 可测试 | 一个函数就是一个测试单元,便于断言 | assert is_prime(7) is True |
| 可组合 | 函数的返回值可以作为另一个函数的参数 | max(map(abs, nums)) |
| 降低复杂度 | 主程序读起来像「步骤清单」,而不是几百行细节 | 把 200 行拆成 10 个各 20 行的函数 |
好函数的三条实践准则:
- 短:一个函数最好只做一件事,正文超过 30 行就该考虑拆分(Python 社区经验值,非硬性)。
- 纯:同样的输入应得到同样的输出,不依赖外部变量、不修改全局状态、不打印。
- 有名字的返回值:用
return交结果,而不是靠print()让外部去猜。
calculate_total();不要用 cal()、doIt() 这类含糊名字。
if __name__ == "__main__": 之外的演示代码,在被 import 时会自动执行。第 23 章的命令行工具会反复用到这一点:
def main():
print("只有直接运行本文件时才执行")
if __name__ == "__main__": # 只有作为脚本直接运行才为 True
main()
参数的五种形式(其实有六种)
Python 函数的参数非常灵活,是本章最需要反复练手的部分。完整清单如下:
| 形式 | 语法 | 调用方式 | 典型用途 |
|---|---|---|---|
| 位置参数(positional) | def f(a, b) | f(1, 2) | 最常见的两个及以上参数 |
| 仅位置参数(positional-only) | def f(a, /, b) | f(1, 2),不能 f(a=1, b=2) | 避免与关键字参数混淆、防误传 |
| 默认参数(default) | def f(a, b=2) | f(1) 或 f(1, 3) | 可选参数 |
| 仅关键字参数(keyword-only) | def f(a, *, b) | 必须写 f(1, b=2) | 可选参数、可扩展性 |
| 变长位置参数 | def f(*args) | f(1, 2, 3) | 参数个数不定,收成 tuple |
| 变长关键字参数 | def f(**kwargs) | f(a=1, b=2) | 参数名不定,收成 dict |
位置参数与关键字参数
调用函数时,参数可以按位置传,也可以按名字传:
# params_basic.py —— 位置传参与关键字传参
def book_info(title, author, year):
"""打印一本书的基本信息。"""
print(f"《{title}》 / {author} / {year} 年")
print("1) 位置传参:靠顺序对应")
book_info("Python 编程", "Mark Lutz", 2021)
print("\n2) 关键字传参:靠名字对应,顺序可以乱")
book_info(year=2021, title="Python 编程", author="Mark Lutz")
print("\n3) 混合:位置参数必须在关键字参数左边")
book_info("流畅的 Python", year=2022, author="Luciano Ramalho")
# 预期输出:
# 1) 位置传参:靠顺序对应
# 《Python 编程》 / Mark Lutz / 2021 年
#
# 2) 关键字传参:靠名字对应,顺序可以乱
# 《Python 编程》 / Mark Lutz / 2021 年
#
# 3) 混合:位置参数必须在关键字参数左边
# 《流畅的 Python》 / Luciano Ramalho / 2022 年
book_info(year=2021, "Python 编程", "Mark Lutz") # SyntaxError
规则是:调用时「位置在前、关键字在后」;定义时「仅位置在前、默认/仅关键字在后」。
默认参数
带默认值的参数必须在无默认值的参数之后,否则解释器无法判断省略哪个:
# params_default.py —— 默认参数与「求值一次」陷阱
def connect(host="localhost", port=8080, timeout=5.0):
return f"{host}:{port} 超时={timeout}s"
print(connect()) # 全用默认值
print(connect("192.168.1.10")) # 只覆盖第一个
print(connect(port=443, timeout=30)) # 用关键字跳过中间的
# 预期输出:
# localhost:8080 超时=5.0s
# 192.168.1.10:8080 超时=5.0s
# localhost:443 超时=30s
print("\n--- 默认值的经典陷阱 ---")
def add_item(item, bucket=[]): # 危险写法!
bucket.append(item)
return bucket
print(add_item("a")) # ['a']
print(add_item("b")) # ['a', 'b'] <-- 上次的结果被保留!
# 正确写法:用 None 作为哨兵
def add_item_ok(item, bucket=None):
if bucket is None: # 注意是「是 None」,不是「非空」
bucket = []
bucket.append(item)
return bucket
print(add_item_ok("a")) # ['a']
print(add_item_ok("b")) # ['b'] <-- 干净
list、dict、set)作默认值只在函数定义时求值一次,之后每次调用都复用同一个对象,导致数据越积越多。永远用 None 做默认值,在函数体内替换。
| 默认值类型 | 能否直接作默认 | 原因 |
|---|---|---|
int / float / str / tuple / frozenset / None | 可以 | 不可变,重复使用没有副作用 |
list / dict / set / bytearray | 不可以 | 可变,会被跨调用共享 |
仅位置参数与仅关键字参数
Python 3.8 引入了 /(斜杠左边的参数只能按位置传),3.0 起就有 *(星号右边的参数只能按关键字传)。两者配合可以写出接口极其清晰的函数:
# params_slash_star.py —— / 与 *
def create_user(name, /, *, email=None, role="user"):
"""创建用户。
name: 仅位置,避免被误写成 name=... 从而混淆
email / role: 仅关键字,让调用者明确意图
"""
return {"name": name, "email": email, "role": role}
u = create_user("张三", email="zhangsan@example.com", role="admin")
print(u)
# 预期输出:
# {'name': '张三', 'email': 'zhangsan@example.com', 'role': 'admin'}
# 下列调用会报错:
# create_user(name="张三") TypeError: name 是仅位置参数
# create_user("张三", "a@b.com") TypeError: email 是仅关键字参数
这条能力极为实用:位置参数数量不定(*args)时,用 * 之后的仅关键字参数承载可选配置,这是标准库大量采用的模式:
# flexible_api.py —— *args 配合仅关键字参数
def log(level, *messages, sep=" | ", end="\n"):
"""level 按位置传,messages 数量任意,格式选项必须写名字。"""
print(sep.join(str(m) for m in messages), end=end, flush=True)
log("INFO", "服务启动", "端口 8080")
log("ERROR", "磁盘已满", "剩余 1%", sep=" -> ", end="\n")
# 预期输出:
# INFO | 服务启动 | 端口 8080
# ERROR -> 磁盘已满 -> 剩余 1%
变长参数:*args 与 **kwargs
# params_var.py —— 收集与展开
def avg(*numbers):
"""接收任意多个位置参数,收集成 tuple。"""
print("收集到:", numbers, type(numbers).__name__)
return sum(numbers) / len(numbers)
print("三个数:", avg(10, 20, 30))
print("五个数:", avg(1, 2, 3, 4, 5))
# 预期输出:
# 收集到: (10, 20, 30) tuple
# 三个数: 20.0
# 收集到: (1, 2, 3, 4, 5) tuple
# 五个数: 3.0
def build_profile(**info):
"""接收任意多个关键字参数,收集成 dict。"""
return info
print(build_profile(name="李四", age=30, city="杭州"))
# 预期输出:{'name': '李四', 'age': 30, 'city': '杭州'}
# ---- 反向:把序列「展开」成多个参数,用 * 或 ** ----
def show(a, b, c):
return f"a={a}, b={b}, c={c}"
data = (7, 8, 9)
print(show(*data)) # 用 * 展开 tuple/list
print(show(**{"a": 1, "b": 2, "c": 3})) # 用 ** 展开 dict(键必须匹配形参名)
# 预期输出:
# a=7, b=8, c=9
# a=1, b=2, c=3
f(*data, **conf) 是编写「灵活包装函数」的标准手法,第 12 章装饰器、第 23 章命令行参数转发都会这样写。
参数传递方式:值传递 vs 对象引用
Python 不存在 Java 那样的值传递(value passing)或引用传递(reference passing)之争,准确的说法是:对象按引用传递,但函数参数一律按值传递。所谓「值」是内存地址的副本。
# passing.py —— 传参语义
def try_to_modify(n, lst):
n = 999 # 只改本地名字的指向,调用方看不到
lst.append("新增") # 改动的是同一个对象的内容,调用方能看到
num = 10
items = ["原有"]
try_to_modify(num, items)
print(num, items)
# 预期输出:10 ['原有', '新增']
# num 没变(int 不可变 + 重新绑定只是改本地名字);items 变了(同一个 list 对象)
def really_replace(lst):
lst[0] = "改写" # 修改元素而不是重新绑定名字
arr = ["原值"]
really_replace(arr)
print(arr) # ['改写']
def shadow(lst):
lst = ["全新"] # 只是让本地名字指向新对象
shadow(arr)
print(arr) # ['改写'] <-- 没变
| 对象类型 | 函数内操作 | 调用方可见 |
|---|---|---|
| 不可变(int/str/tuple) | n = 999 | 否 |
| 可变(list/dict/set) | lst.append(1) | 是 |
| 可变 | lst = [1](重新绑定) | 否 |
| 可变 | lst[:] = [1](原地改内容) | 是 |
lst = list(lst);写库函数时这几乎是必须的习惯。
返回值:return、多返回值与元组
return 的行为
# return_demo.py —— return 的四种情况
def with_return():
return "有返回值"
def without_return():
return # 只写 return,或干脆不写
def early_exit(n):
if n < 0:
return "负数"
if n == 0:
return "零"
return "正数"
print(repr(with_return())) # '有返回值'
print(repr(without_return())) # None <-- 隐式返回 None
print(early_exit(-5), early_exit(0), early_exit(3))
# 负数 零 正数
# return 立刻结束函数(像 break 一样),常用于「提前退出」保护
def safe_divide(a, b):
if b == 0:
return None # 不能除,返回 None
return a / b
print(safe_divide(10, 2), safe_divide(10, 0))
# 5.0 None
多返回值:用元组解包
Python 没有 C/C++ 那样的指针出参。返回多个值的方式是返回一个元组,再解包:
# multi_return.py —— 多返回值
def min_max(nums):
"""返回最小值、最大值和总和。"""
return min(nums), max(nums), sum(nums) # 逗号即构造元组
lo, hi, total = min_max([8, 3, 19, 42, 7])
print(f"最小={lo} 最大={hi} 总和={total}")
# 最小=3 最大=42 总和=79
# 只想取一部分:用下标或 _ 丢弃
smallest = min_max([1, 2, 3])[0]
print(smallest) # 1
a, _, c = min_max([5, 6, 7])
print(a, c) # 5 7 (中间的 6 用 _ 丢弃)
# 元组可比较、可作字典键,适合当复合返回值的封装
print(min_max([3, 1, 2]) == (1, 3, 6)) # True
# ---- 真实场景:解析一行日志 ----
def parse_log(line):
"""把 '2024-05-01 12:30 INFO 服务已启动' 拆成结构化数据。"""
date, time, level, *rest = line.split()
message = " ".join(rest)
return {"date": date, "time": time, "level": level, "message": message}
log = parse_log("2024-05-01 12:30 INFO 服务已启动")
for k, v in log.items():
print(f"{k:8s} = {v}")
# 预期输出:
# date = 2024-05-01
# time = 12:30
# level = INFO
# message = 服务已启动
x = f() 而不是 a, b = f(),拿到的就是一个元组。元组长度固定且语义清晰,不建议返回长度为 6 的元组;元素超过 3 个时应改用 dataclass 或 NamedTuple(第 12、13 章详述)。
作用域与 LEGB 规则
「这个名字在哪里能查到」由 LEGB 规则决定,按顺序查找四个位置,找到即停:
| 字母 | 名称 | 含义 | 典型例子 |
|---|---|---|---|
| L | Local | 局部作用域:函数内部、类内部赋值的名字 | 函数体里的 total = 0 |
| E | Enclosing | 外层函数作用域(仅对嵌套函数有意义) | 闭包里的 rate |
| B | Global | 模块顶层作用域 | 模块里的 TAX_RATE |
| G | Built-in | 内置作用域 | print、len、max |
# legb.py —— 四层作用域
TAX_RATE = 0.06 # G:Global,模块级常量
def outer(rate): # 参数 rate 属于 Enclosing 层
rate = rate # 注意:这行只是给参数重新绑定,仍在 L 层
def inner(amount):
"""L 层没有 rate,于是依次查 E(函数参数)、G。"""
return amount * (1 + rate)
return inner
def demo_legb():
"""演示 global 与 nonlocal 的差别。"""
counter = 0 # 外层函数的局部变量
def bump():
nonlocal counter # 声明「我要改外层的 counter」
counter += 1
return counter
print("bump 前:", counter)
print("bump 后:", bump())
return counter
c = outer(0.1)(100) # outer 返回 inner,立即用 100 调用
print("含税价格:", c) # 110.0
print("内置作用域:", len("Python")) # 6
print(demo_legb()) # 1
global 与 nonlocal
# scope_kw.py —— global 修改全局,nonlocal 修改外层局部
counter = 0
def use_global():
global counter # 声明为全局
counter += 1 # 不写 global 会报 UnboundLocalError
def make_accumulator():
"""返回一个能不断累加的闭包函数。"""
total = 0
def add(n):
nonlocal total # 指向外层函数的 total,而不是新建同名局部变量
total += n
return total
return add
use_global()
use_global()
print("全局 counter =", counter) # 2
acc = make_accumulator()
print(acc(10), acc(20), acc(30)) # 10 30 60
UnboundLocalError:
count = 0
def bad():
print(count) # 报 UnboundLocalError
count = 1 # 这行让 count 成了局部变量
def good():
global count
print(count) # 正常读到 0
count = 1
global。函数式风格(返回新状态)比修改外部状态更容易测试和并行,第 15 章并发编程时会再次印证这一点。
闭包:函数记住它的出生环境
闭包(closure)是一个函数加上它定义时所引用的外层变量。Python 用「函数对象里存一个 __closure__ 元组」来实现它,内存中的引用让外层函数返回后,那些变量依然存活。
# closure_demo.py —— 闭包保存状态
def make_counter(start=0):
"""返回一个闭包函数,每次调用把内部计数 +1。"""
n = start
def increment(step=1):
nonlocal n
n += step
return n
return increment # 只返回函数本身,不返回 n
c1 = make_counter() # 第一台独立机器
c2 = make_counter(100) # 第二台机器,起点 100
print(c1(), c1(), c1()) # 1 2 3
print(c2(), c2()) # 101 102
print("互不影响:c1 再调一次 ->", c1()) # 4
print("\n--- 闭包的内部结构 ---")
print("__closure__ 长度:", len(c1.__closure__))
cell = c1.__closure__[0]
print("cell 里存的值:", cell.cell_contents)
# __closure__ 长度: 1
# cell 里存的值: 4
闭包的三大用途:
- 封装私有状态:外部无法直接改动内部变量,只能通过函数操作。
- 带状态的装饰器:
functools.lru_cache、functools.cached_property都基于闭包。 - 函数工厂(function factory):一次生成一批参数固定的函数。
# closure_uses.py —— 真实场景
import time
def make_timer(unit="秒"):
"""生成一个「距上次报时多少时间已过」的闭包。"""
last = time.time()
def since(tag="计时"):
nonlocal last
now = time.time()
delta = now - last
last = now
return f"[{tag}] {delta:.6f} {unit}"
return since
t = make_timer()
busy_loop = sum(i * i for i in range(300000))
print(t("计算完成"))
print(t("计算完成"))
# ---- 闭包 + 装饰器:带参数的装饰器模板 ----
def repeat(times):
"""返回一个装饰器;装饰器本身是「接收装饰器的函数」。"""
def decorator(func):
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def greet(name):
"""打招呼。"""
print(f"你好,{name}!")
return "done"
print(greet("王小明"))
# 预期输出(问候三次):
# 你好,王小明!
# 你好,王小明!
# 你好,王小明!
# done
# 错误写法:三个函数都用 n=2
funcs = [lambda: n for n in range(3)]
print([f() for f in funcs]) # [2, 2, 2]
# 正确写法 1:默认参数在定义时求值
funcs = [lambda n=n: n for n in range(3)]
print([f() for f in funcs]) # [0, 1, 2]
# 正确写法 2:工厂函数(推荐)
def make(n):
return lambda: n
funcs = [make(n) for n in range(3)]
print([f() for f in funcs]) # [0, 1, 2]
inspect.getclosurevars(func) 查看。
递归:函数调用自己
递归(recursion)是函数直接或间接调用自身。它把「大问题」逐步拆成「同类小问题」,直到可以直接求解的基本情况(base case)。
递归三要素
- 基本情况:递归终止的条件,必须存在且能被触发。
- 递归步骤:把问题规约成更小的同类问题。
- 递归出口:保证参数最终朝着基本情况收敛(每次都更接近终点)。
# recursion_factorial.py —— 阶乘
def factorial(n):
"""n 的阶乘:n! = n × (n-1) × ... × 1。"""
if n < 0:
raise ValueError("阶乘不接受负数")
if n <= 1: # (1) 基本情况
return 1 # 0! = 1, 1! = 1
return n * factorial(n - 1) # (2) 递归步骤(规约到更小的 n)
print(factorial(5)) # 120
print(factorial(0)) # 1
# 等价的迭代写法(不会爆栈、更快)
def factorial_iter(n):
result = 1
while n > 1:
result *= n
n -= 1
return result
print(factorial_iter(5)) # 120
# 尾递归形式(Python 不会自动优化,需要靠手写 while)
def factorial_tail(n, acc=1):
while n > 1:
n, acc = n - 1, acc * n
return acc
print(factorial_tail(5)) # 120
递归的经典应用
# recursion_apps.py —— 斐波那契、汉诺塔、树遍历
def fib(n, memo=None):
"""带记忆化的斐波那契,避免指数级重复计算。"""
if memo is None:
memo = {}
if n <= 1:
return n
if n not in memo:
memo[n] = fib(n - 1, memo) + fib(n - 2, memo)
return memo[n]
print("fib(30) =", fib(30)) # 832040(朴素递归需 100 万+ 次调用,记忆化后仅 30 次)
def hanoi(n, src, dst, aux):
"""汉诺塔:n 个盘子从 src 经 aux 移到 dst,返回步骤列表。"""
if n == 0:
return []
steps = hanoi(n - 1, src, aux, dst)
steps.append(f"{src} -> {dst} ({n})")
steps.extend(hanoi(n - 1, aux, dst, src))
return steps
for step in hanoi(3, "A", "C", "B"):
print(step)
# 预期输出:
# A -> B (1)
# A -> C (2)
# B -> C (1)
# A -> C (3)
# B -> A (1)
# C -> A (2)
# B -> A (3)
def flatten(nested):
"""递归展开任意层级的嵌套列表。"""
out = []
for item in nested:
if isinstance(item, list):
out.extend(flatten(item))
else:
out.append(item)
return out
print(flatten([1, [2, [3, [4, [5, 6]]]], 7]))
# 预期输出:[1, 2, 3, 4, 5, 6, 7]
递归的限制与 Python 的递归上限
# recursion_limit.py —— 查看递归深度限制
import sys
print("默认递归上限:", sys.getrecursionlimit()) # 通常为 1000
def deep(n):
return 1 if n == 0 else 1 + deep(n - 1)
print(deep(500)) # 正常
# print(deep(2000)) # RecursionError
# 危险操作:把上限调得太大可能让进程直接崩溃(栈溢出,不可捕获)
# sys.setrecursionlimit(100000) # 一般只在框架内部使用
RecursionError: maximum recursion depth exceeded。正确解法是改写成显式栈的迭代版本,而不是盲目调大上限。
| 维度 | 递归 | 迭代 |
|---|---|---|
| 可读性 | 树、分治、回溯类问题更直观 | 线性流程更自然 |
| 性能 | 函数调用开销大 | 通常更快 |
| 内存 | 占调用栈,受递归上限限制 | 可精确控制栈大小 |
| 深度 | 上限约 1000 | 受内存限制,深度更大 |
函数即对象:把函数当值传递
在 Python 里,函数是对象(first-class object):可以赋值给变量、放进列表或字典、作参数传入、从另一个函数返回、存入对象属性。这意味着你可以编写「接收函数」的高阶函数(higher-order function)。
# first_class.py —— 函数是一等公民
def double(x):
return x * 2
def triple(x):
return x * 3
# 1) 存进容器
ops = {"double": double, "triple": triple}
print(ops["triple"](5)) # 15
# 2) 存进列表,按需选择
funcs = [double, triple]
print([f(4) for f in funcs]) # [8, 12]
# 3) 函数可以作参数
def apply_all(func, values):
return [func(v) for v in values]
print(apply_all(double, [1, 2, 3])) # [2, 4, 6]
print(apply_all(triple, [1, 2, 3])) # [3, 6, 9]
# 4) 函数可以作返回值(工厂)
def get_adder(n):
def adder(x):
return x + n
return adder
add10 = get_adder(10)
print(add10(5)) # 15
# 5) 交换函数的行为 —— 不改函数本身,只改引用
def square(x):
return x ** 2
func = square
print(func(5)) # 25
func = triple
print(func(5)) # 15 (square 本身没变,只是名字指向了 triple)
print(square(5)) # 25
# 6) 匿名函数 + sorted 的 key(最常见的函数传递)
names = ["李四", "王", "张三丰", "陈小明"]
print(sorted(names, key=len)) # ['王', '李四', '张三丰', '陈小明']
print(sorted(names, key=len, reverse=True)) # ['张三丰', '陈小明', '李四', '王']
print(sorted(["banana", "kiwi", "apple"], key=str.upper)) # ['apple', 'banana', 'kiwi']
# 7) 统计函数自身的属性
print(double.__name__) # double
print(double.__doc__) # None(本例未写文档字符串)
print(apply_all.__doc__ is not None) # True
实战:用「函数作为数据」重写业务逻辑
# strategy.py —— 策略模式的最简形态(第 12 章会系统展开)
ORDERS = [
{"item": "手机壳", "price": 30.0, "member": "gold", "qty": 2},
{"item": "数据线", "price": 25.0, "member": "normal", "qty": 3},
{"item": "耳机", "price": 199.0, "member": "silver", "qty": 1},
]
DISCOUNTS = { # 策略表:会员等级 -> 折扣函数
"gold": lambda p: p * 0.80,
"silver": lambda p: p * 0.90,
"normal": lambda p: p * 1.00,
}
def settle(order, discount_fn):
"""通用结算:折扣策略由调用方注入。"""
unit = discount_fn(order["price"])
return round(unit * order["qty"], 2)
def total_payable(order, strategy="tiered"):
"""按名调用策略,可动态切换。"""
if strategy == "tiered": # 满 500 减 50
amount = order["price"] * order["qty"]
return round(amount - 50 if amount >= 500 else amount, 2)
return settle(order, DISCOUNTS[order["member"]])
for o in ORDERS:
t1 = total_payable(o, "tiered")
t2 = total_payable(o)
print(f"{o['item']:4s} 满减={t1:8.2f} 会员折扣={t2:8.2f}")
print("\n汇总会员折扣:", round(sum(total_payable(o) for o in ORDERS), 2))
# 预期输出:
# 手机壳 满减= 60.00 会员折扣= 48.00
# 数据线 满减= 75.00 会员折扣= 75.00
# 耳机 满减= 149.00 会员折扣= 179.10
#
# 汇总会员折扣:302.1
functools.partial 可以「预填」参数,把任意函数适配成另一个签名,常用于回调和性能敏感的循环:
from functools import partial
def send(url, payload, timeout=5, retries=3):
return f"POST {url} payload={payload} timeout={timeout} retries={retries}"
# 先绑定 url 与 payload
send_api = partial(send, "https://api.example.com/v1/items")
print(send_api({"id": 1}))
print(send_api({"id": 2}, timeout=10)) # 后面仍可继续覆盖
func.__name__、func.__doc__、func.__module__、func.__qualname__,还能赋值自定义属性 func.tag = "v2"。这是 Flask 路由注册、日志库挂标记的常用手法。
实战:用函数组织一个「文本统计器」
把前面所有知识串起来,写一个完整可运行的小工具:
text_stats.py —— 组合闭包 + 高阶函数 + 多返回值
"""文本统计器:演示参数五种形式、多返回值、闭包与函数即对象。"""
import re
from collections import Counter
# ---------- 1. 参数形式全家桶 ----------
def analyze(text, *, strip_punct=True, ignore_case=True, top=5):
"""分析一段文本。
text: 位置参数
strip_punct / ignore_case / top: 仅关键字参数(强制调用者写清意图)
"""
result = {"raw_len": len(text)}
content = text
if strip_punct:
content = re.sub(r"[^\w\s]", "", content, flags=re.UNICODE)
result["punct_removed"] = len(text) - len(content)
if ignore_case:
content = content.lower()
words = content.split()
result["word_count"] = len(words)
result["uniq_count"] = len(set(words))
result["char_count"] = len(content.replace(" ", ""))
# 多返回值 + 元组解包
longest, shortest = max(words, key=len), min(words, key=len)
result["longest_word"] = longest
result["shortest_word"] = shortest
# 闭包:把「取 top N」做成内部函数,携带 top 参数
def top_n():
return Counter(words).most_common(top)
result["top_words"] = top_n()
return result
# ---------- 2. 变长参数:接收任意多段文本 ----------
def merge(*texts, separator="\n\n"):
return separator.join(t.strip() for t in texts if t.strip())
def report(**options):
"""接收任意关键字参数,返回配置摘要。"""
defaults = {"strip_punct": True, "ignore_case": True, "top": 3}
cfg = {**defaults, **options} # ** 解包合并字典
return cfg
# ---------- 3. 递归:统计嵌套结构深度 ----------
def depth(obj):
"""递归计算嵌套结构的深度。"""
if isinstance(obj, dict):
return 1 + max((depth(v) for v in obj.values()), default=0)
if isinstance(obj, (list, tuple, set)):
return 1 + max((depth(v) for v in obj), default=0)
return 0
# ---------- 4. 高阶函数:对每个值应用处理函数 ----------
def pipeline(value, *funcs):
"""把若干函数串成管道,依次施加。"""
for f in funcs:
value = f(value)
return value
SAMPLE = """Python is a programming language.
Python 入门,简单又强大!Python rocks.
Do you like Python? Python, Python, Python!"""
if __name__ == "__main__":
stats = analyze(SAMPLE, top=3)
print("=== 文本统计 ===")
for k, v in stats.items():
print(f"{k:16s}: {v}")
print("\n=== 配置合并(**kwargs)===")
print(report(top=5, ignore_case=False))
print("\n=== 合并多段(*args)===")
print(repr(merge("第一段", "第二段", separator=" | ")))
print("\n=== 嵌套深度(递归)===")
nested = {"a": [1, {"b": {"c": {"d": 1}}}]}
print(depth(nested))
print("\n=== 函数管道(高阶函数)===")
print(pipeline(SAMPLE, str.strip, len))
预期输出(文本部分因源码中的换行略有差异):
=== 文本统计 ===
raw_len : 118
punct_removed : 5
word_count : 20
uniq_count : 13
char_count : 84
longest_word : programming
shortest_word : a
top_words : [('python', 8), ('is', 2), ('like', 2)]
=== 配置合并(**kwargs)===
{'strip_punct': True, 'ignore_case': False, 'top': 5}
=== 合并多段(*args)===
'第一段 | 第二段'
=== 嵌套深度(递归)===
4
=== 函数管道(高阶函数)===
115
本章小结
- 函数定义用
def,缩进体是函数体;定义不执行,调用才执行;用if __name__ == "__main__":保护脚本入口。 - 参数六形态:位置参数、仅位置(
/)、默认参数、仅关键字(*)、*args、**kwargs。调用顺序规则是「位置在前、关键字在后」。 - 可变对象禁止作默认值,用
None哨兵 + 函数体内初始化。 - 传参语义:对象按引用传递,参数按值传递;重新绑定名字不影响调用方,修改内容会影响。
- return 立即结束函数;无 return 返回
None;多返回值本质是元组,调用方解包。 - LEGB 决定名字查找:局部 → 外层 → 全局 → 内置。
global改全局,nonlocal改外层局部。 - 闭包= 函数 + 外层环境,能保存私有状态、制造函数工厂;循环里建闭包要用默认参数或工厂函数快照变量。
- 递归三要素:基本情况、递归步骤、递归出口;默认深度上限约 1000,超限改写为显式栈的迭代版。
- 函数是一等对象:可赋值、入容器、作参数、返回值、带属性;由此可得高阶函数、装饰器、策略模式与
functools.partial。
练习题
- 参数形式综合。定义函数
make_profile(name, /, *, age=18, city="北京", **extra),其中name只能按位置传,age/city必须写关键字,其余任意关键字收集进extra。调用它传入"李雷"、age=20、hobby="摄影",验证返回字典内容。 - 修 bug。下面代码第二次调用返回的是
['x', 'y']而不是['y'],请说明原因并给出两种修正方式:def push(item, lst=[]): lst.append(item) return lst参考答案:默认列表在函数定义时创建一次,之后所有调用共享同一对象。修正一:默认None,函数体if lst is None: lst = []。修正二:def push(item, lst=())后return lst + (item,)(不可变默认值,但写法不同)。 - 多返回值。实现
stats(nums),返回(总和, 平均值, 最大值, 最小值, 方差)五个结果,方差用总体公式Σ(x-μ)²/n。对[2, 4, 4, 4, 5, 5, 7, 9]测试,并用解包接收;再故意只取其中两个值。 - 闭包状态机。写
make_bank(balance=0),返回一个闭包op(cmd, amount),支持deposit(存)、withdraw(取,余额不足返回False)、balance(查询)三条命令,并演示两个不同的「银行账户」余额互不干扰。参考答案:def make_bank(balance=0): def op(cmd, amount=0): nonlocal balance if cmd == "deposit": balance += amount elif cmd == "withdraw": if balance < amount: return False balance -= amount elif cmd == "balance": pass else: raise ValueError(cmd) return balance return op acc = make_bank(100) print(acc("withdraw", 200)) # False print(acc("deposit", 50)) # 150 print(acc("balance")) # 150 - 递归改造。把
def total(n): return n if n <= 0 else n + total(n - 1)改写为迭代版本,并再写一个「尾递归模拟」版本(用while+ 累加器)。三者在total(1000)上结果应一致。参考答案:迭代版while n > 0: s += n; n -= 1;尾递归版while n > 0: n, s = n - 1, s + n。注意:total(1000)用朴素递归在部分解释器上会触及递归上限。 - 高阶函数重写。把「列表去重并保持顺序」分别用三种方式实现:①
set;②for+list.append;③ 用sorted配合key与set去重。比较三者在顺序是否保留上的差异。 - LEGB 辨析。下面代码打印什么?为什么?
x = "全局" def outer(): x = "外层" def middle(): def inner(): print(x) inner() middle() outer()参考答案:打印「外层」。inner的 L 层没有x,E 层找到middle的作用域(其父是outer),于是取到outer的x,不会继续查到全局。 - 参数陷阱。解释下面两行输出的差异,并写出「如何只给 100 传参而让其余用默认值」的写法:
def f(a, b, c=3, *, d=4): return (a, b, c, d) print(f(1, 2, 3, 5)) # 第 1 行 print(f(1, 2, d=9)) # 第 2 行参考答案:第 1 行传了 4 个实参,其中第 4 个位置值落到仅关键字参数d,结果(1, 2, 3, 5)。第 2 行(1, 2, 3, 9)。 - 综合实战。写一个
validate(validator_dict, *values, strict=True, **options):按validator_dict中「字段名 → 校验函数」的顺序校验values,strict=True时任一失败立即抛异常,否则收集所有错误并返回。至少写 3 个校验函数(not_empty、is_int、in_range(min,max)闭包工厂)。参考答案要点:校验函数统一签名f(value) -> bool 或 抛异常;用try/except捕获逐项失败;in_range用闭包捕获上下界:def in_range(lo, hi): def check(v): if not lo <= v <= hi: raise ValueError(f"{v} 不在 [{lo}, {hi}]") return True return check