Python 函数的设计远比表面看起来灵活。从可变参数的解包,到闭包对词法环境的捕获,再到装饰器对函数的无侵入扩展(前面章节已经详细介绍),以及生成器对内存的极致节省 —— 这些特性构成了 Python 函数式编程的基石。
本章将逐一讲透这些概念,读完之后,你不仅知道语法怎么写,更知道什么时候该用、什么时候不该用。
Python 函数支持位置参数和关键字参数。但有时候,你事先不知道调用者会传多少个参数。*args 和 **kwargs 就是解决这个问题的万能工具。
调用函数时,依靠传入顺序匹配形参的实参,就是位置参数。
def log_args(first, *args):
print(f"first={first}, type={type(first)}")
print(f"args={args}, type={type(args)}")
log_args(1, 2, 3, 4)
# first=1, type=<class 'int'>
# args=(2, 3, 4), type=<class 'tuple'>*args 在函数定义中是一个元组,接收所有多余的位置参数。命名习惯叫 args,但你可以改。first 前面的参数按正常规则匹配,剩下的全部进 args。
调用函数时,以“参数名=值”形式传入,就是关键字参数。关键字参数不受顺序约束,但必须放在位置参数之后。例如:
def log_kwargs(first, **kwargs):
print(f"first={first}")
print(f"kwargs={kwargs}, type={type(kwargs)}")
log_kwargs(1, x=10, y=20, name="test")
# first=1
# kwargs={'x': 10, 'y': 20, 'name': 'test'}, type=<class 'dict'>**kwargs 是一个字典,接收所有未在形参列表中显式声明的关键字参数。命名习惯叫 kwargs。
def universal(*args, **kwargs):
"""接收任意参数,打印并原样转发"""
print(f"Positionals: {args}")
print(f"Keywords: {kwargs}")
universal(1, 2, 3, a=10, b=20)
# Positionals: (1, 2, 3)
# Keywords: {'a': 10, 'b': 20}universal(*args, **kwargs) 是包装器和代理函数的基础模式。它自己不处理参数,只是透传。
* 和 ** 在函数定义中是"打包",在调用时是"解包"(unpacking)。这是两个方向的操作,不要混淆。例如:
def calc(a, b, c, d=0):
return a + b + c + d
# 用列表/元组解包
nums = [1, 2, 3]
print(calc(*nums)) # 6 (等价于 calc(1, 2, 3))
# 用字典解包
params = {"a": 1, "b": 2, "c": 3, "d": 10}
print(calc(**params)) # 16 (等价于 calc(a=1, b=2, c=3, d=10))
# 混合解包
print(calc(*[1, 2], **{"c": 3, "d": 5})) # 11解包在代理函数和装饰器里极其常用:
def proxy(*args, **kwargs):
# 把接收到的参数原样转发给底层函数
return real_func(*args, **kwargs)Python 3 允许在参数列表中用 * 作为分隔符,其后的参数必须传关键字:
def create_user(name, *, age=0, active=True):
"""age 和 active 必须关键字传参"""
return {"name": name, "age": age, "active": active}
print(create_user("Alice", age=30)) # 正确
print(create_user("Bob")) # 正确,用默认值
create_user("Charlie", 30) # TypeError: 30 不能作为位置参数给 age这个特性在 API 设计中有奇效:强制调用者显式写出参数名,代码可读性大幅提升。比如 sorted(data, key=..., reverse=...) 里的 key 和 reverse 就是关键字-only 的惯例。
def greet(name, /, greeting="Hello"):
"""name 必须位置传参,greeting 可位置可关键字"""
return f"{greeting}, {name}!"
greet("Alice") # 正确
greet("Alice", "Hi") # 正确
greet(name="Alice") # TypeError: name 不能关键字传参/ 分隔符前的参数必须位置传参。这个特性在底层库和 C 扩展的 Python 绑定中常见,日常业务代码用得不多,但看到 def func(a, /, b, *, c) 这种签名时要知道它的含义。
闭包(Closure)是指一个函数记住了它被定义时的环境,即使那个环境里的变量已经不在当前作用域中。Python 的嵌套函数天然支持闭包。
def make_multiplier(n: float):
"""返回一个乘以 n 的函数"""
def multiplier(x: float) -> float:
return x * n
return multiplier
times3 = make_multiplier(3)
times5 = make_multiplier(5)
print(times3(10)) # 30
print(times5(10)) # 50
# 查看闭包保存的变量
print(times3.__closure__[0].cell_contents) # 3
print(times5.__closure__[0].cell_contents) # 5make_multiplier 返回的 multiplier 函数 "记住" 了 n 的值。即使 make_multiplier 已经执行完毕,n 仍然被闭包持有,不会随着栈帧销毁而消失。__closure__ 是一个元组,每个元素是 cell 对象,通过 cell_contents 可以读取捕获的值。
def make_power(base: int):
"""延迟计算幂次"""
def power(exp: int) -> int:
return base ** exp
return power
square = make_power(2)
cube = make_power(10)
print(square(10)) # 1024 (2^10)
print(cube(2)) # 100 (10^2)这是初学者最容易踩的坑:
def make_functions():
"""错误:所有函数都返回 9"""
funcs = []
for i in range(3):
def inner():
return i # i 是延迟绑定,不是值捕获!
funcs.append(inner)
return funcs
f0, f1, f2 = make_functions()
print(f0(), f1(), f2()) # 都输出 2 2 2原因:i 在闭包里是引用绑定,不是值拷贝。循环结束后 i 的值是 2,三个函数都指向同一个 i。修复方法有两种:
方法一:默认参数绑定值
def make_functions():
funcs = []
for i in range(3):
def inner(x=i): # x 在定义时绑定当前 i 的值
return x
funcs.append(inner)
return funcs
f0, f1, f2 = make_functions()
print(f0(), f1(), f2()) # 都输出 0 1 2方法二:工厂函数显式捕获
def make_functions():
funcs = []
for i in range(3):
funcs.append((lambda x: lambda: x)(i))
return funcs
f0, f1, f2 = make_functions()
print(f0(), f1(), f2()) # 都输出 0 1 2注意,更推荐方法一,它利用默认参数在函数定义时求值的特性,把 i 的值冻结在 x 里。
默认情况下,闭包内层函数只能读取外层变量,不能修改。如果需要修改,用 nonlocal 声明:
def make_counter(start: int = 0):
count = start
def increment(step: int = 1):
nonlocal count # 声明修改外层变量,不是创建局部变量
count += step
return count
def reset():
nonlocal count
count = start
return count
return increment, reset
inc, reset = make_counter(10)
print(inc()) # 11
print(inc(5)) # 16
print(reset()) # 10
print(inc()) # 11nonlocal 是 Python3 新增的关键字。在 Python 2 中,只能用可变对象(如列表或字典)来间接修改外层状态。nonlocal 让闭包的状态管理更加直观。
生成器(Generator)是 Python 处理大数据的利器。它不像列表那样一次性把所有数据加载到内存,而是逐个产出(yield)值,用的时候才生成。
Python 中,关键字 yield 用来创建生成器(generator),暂停函数、返回值,下次调用从暂停处继续执行,不用一次性把所有数据放进内存。例如:
def gen():
print("A")
yield 1
print("B")
yield 2
print("C")
g = gen() # 仅仅创建生成器,函数内部代码一行都不执行!
val = next(g)
# 执行:打印A → 碰到 yield 1 → 暂停,返回 1
print(val) # 1
val = next(g)
# 从暂停位置继续:打印B → 碰到 yield 2 → 暂停,返回2
print(val) # 2
val = next(g)
# 继续执行:打印C → 函数跑完,没有yield
# 抛出 StopIteration注意,next(生成器对象) 驱动生成器执行代码,直到遇到下一个 yield,取出产出的值;没有更多 yield 就抛 StopIteration。
import sys
# 列表:一次性创建 100 万个整数
nums_list = [i for i in range(1000000)]
print(f"List size: {sys.getsizeof(nums_list)} bytes") # 8448728 bytes
# 通常约 800 万字节
# 生成器:几乎不占用内存
def gen_nums():
for i in range(1000000):
yield i
nums_gen = gen_nums()
print(f"Generator size: {sys.getsizeof(nums_gen)} bytes") # 192 bytes
# 通常约 190 字节
# 取值时才计算
print(next(nums_gen)) # 0
print(next(nums_gen)) # 1生成器对象只保存当前状态(执行到哪个 yield、局部变量是什么),而不是全部数据。处理百万级数据时,生成器能避免内存溢出。
列表推导 [x for x in range(10)] 的生成器版本是 (x for x in range(10)),用圆括号而不是方括号:
# 生成器表达式
squares = (x * x for x in range(1000000))
print(sum(squares)) # 333332833333500000
# 对比列表推导,内存占用差异巨大
squares_list = [x * x for x in range(1000000)]
print(sum(squares_list)) # 结果一样,但内存用了列表的全部空间生成器表达式只能遍历一次,遍历完后自动耗尽。如果需要多次使用,要么转成列表,要么重新创建生成器。
yield from 可以把一个生成器的产出委托给另一个生成器,简化嵌套:
def sub_generator(start, end):
for i in range(start, end):
yield i
def main_generator():
yield "A"
yield from sub_generator(1, 4) # 等价于 yield 1; yield 2; yield 3
yield "B"
yield from sub_generator(10, 13)
yield "C"
for item in main_generator():
print(item, end=" ")
# 输出: A 1 2 3 B 10 11 12 Cyield from 还能让子生成器把 return 的值传回父生成器:
def counter():
total = 0
for i in range(1, 4):
total += i
yield i
return total # 最终返回值
def main():
result = yield from counter()
print(f"Total: {result}")
yield "done"
for item in main():
print(item)
# 1
# 2
# 3
# Total: 6
# done生成器不是单向的产出通道,调用者可以通过 .send() 向生成器发送数据:
def running_average():
total = 0.0
count = 0
average = None
while True:
term = yield average # 产出当前平均值,等待接收新值
total += term
count += 1
average = total / count
avg = running_average()
next(avg) # 或 avg.send(None),启动生成器
print(avg.send(10)) # 10.0
print(avg.send(20)) # 15.0
print(avg.send(30)) # 20.0
avg.close() # 终止生成器说明:
.send(value) 把值传给 yield 表达式左侧的变量,同时推进到下一个 yield;
第一次启动必须用 next() 或 send(None),因为此时生成器还没执行到 yield;
.close() 发送 GeneratorExit 异常,让生成器执行清理逻辑;
.throw(exc) 向生成器抛出异常,用于错误注入和测试。
这是生成器最常见的实战场景:
def read_large_file(filepath: str, chunk_size: int = 8192):
"""逐块读取大文件,避免一次性载入内存"""
with open(filepath, 'rb') as f:
while True:
chunk = f.read(chunk_size)
if not chunk:
break
yield chunk
# 逐行处理(更常用)
def read_lines(filepath: str):
"""逐行读取,支持文件对象被垃圾回收前的清理"""
with open(filepath, 'r', encoding='utf-8') as f:
for line in f:
yield line.strip()
# 使用:每次读取64字节
for chunk in read_large_file("huge.log", chunk_size=64):
print(chunk)
# 使用:处理一个 10GB 的日志文件,内存只占一行
for line in read_lines("huge.log"):
if "POST" in line:
print(line)with open(...) 放在生成器函数里有一个精妙之处:文件对象的生命周期与生成器绑定。当生成器被垃圾回收或 close() 时,with 语句块确保文件被关闭。这比返回列表的函数更优雅——后者必须在所有数据处理完毕后才关闭文件。
lambda 用来创建匿名函数(没有名字的临时小函数)。语法如下:
lambda 参数列表: 表达式注意:
lambda 没有 return,表达式的结果自动作为返回值。
只能写一条表达式,不能写 if 块、for循环、while、try 等多行代码。
适合简单逻辑、临时回调、配合 sorted/map/filter。
下面是 Lambda 简单示例:
# 一个参数,实现平方
square = lambda x: x * x
print(square(5)) # 25
# 多参数,实现加法
add = lambda x, y: x + y
print(add(2, 3)) # 5
# lambda 不能写语句,只能写表达式
# lambda x: print(x) # 语法上可行,但不太推荐,可读性差注意,lambda 适合写简单的、用完即丢的函数。如果逻辑超过一行,应该写成普通函数。PEP 8 建议:"如果 lambda 体超过一行,用 def 定义。"
map(函数, 可迭代对象) 函数用于把可迭代对象里的每一个元素,依次传入函数执行,收集返回结果,最终返回一个 map 对象(迭代器)。
例如:使用 lambda 作为函数。
nums = [1, 2, 3, 4, 5]
# 用 lambda 函数实现对列表每个组求平方
squares = list(map(lambda x: x * x, nums))
print(squares) # [1, 4, 9, 16, 25]
# 用内置函数
# str(object):把任意对象转换成字符串形式。
str_nums = list(map(str, nums))
print(str_nums) # ['1', '2', '3', '4', '5']
# 多序列映射,列表对应位置的值相加
a = [1, 2, 3]
b = [10, 20, 30]
sums = list(map(lambda x, y: x + y, a, b))
print(sums) # [11, 22, 33]map 返回迭代器,惰性求值。list(map(...)) 把它转成列表。如果只需要遍历一次,不必 list() 化,直接用 for 循环更省内存。
filter(判断函数, 可迭代对象) 函数用于遍历序列每一个元素,交给判断函数;函数返回 True 就保留元素,False 丢弃;最终返回一个迭代器。
例如:使用 lambda 函数作为判断函数
nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# 过滤出偶数
evens = list(filter(lambda x: x % 2 == 0, nums))
print(evens) # [2, 4, 6, 8, 10]
# 用 None 过滤 falsy 值
values = [0, 1, '', 'hello', None, [], [1, 2]]
truthy = list(filter(None, values))
print(truthy) # [1, 'hello', [1, 2]]sorted(iterable, key=None, reverse=False) 函数用于排序,使用迭代对象中每个对象中 key 指定元素坐做比较,不改变原始元素,只改变排列顺序。
参数说明:
iterable:列表、元组、字符串、生成器等可迭代对象
key:函数,用来指定用什么依据排序。对序列里每一个元素先执行 key(元素),拿到一个 “排序键”,程序只根据这个键大小排序;原始元素本身不变,只是按照键的顺序重新排列元素。
reverse=False:升序;reverse=True:降序
示例:使用 lambda 函数作为 key
data = [
("Alice", 30, "Engineer"),
("Bob", 25, "Designer"),
("Charlie", 35, "Manager"),
("David", 25, "Intern"),
]
# 按年龄排序
by_age = sorted(data, key=lambda x: x[1])
print(by_age)
# [('Bob', 25, 'Designer'), ('David', 25, 'Intern'), ...]
# 按年龄升序,年龄相同按姓名升序
by_age_name = sorted(data, key=lambda x: (x[1], x[0]))
# 或者:by_age_desc = sorted(data, key=lambda x: x[1], reverse=True)
print(by_age_name)
# 按字符串长度排序
words = ["apple", "pie", "banana", "kiwi"]
by_len = sorted(words, key=len)
print(by_len) # ['pie', 'kiwi', 'apple', 'banana']注意,sorted 返回新列表,不修改原列表。list.sort() 是原地排序,返回 None。key 函数只被调用一次 per 元素,结果缓存后用于比较,这是 sorted 高效的原因。
reduce() 用来对序列持续累积运算。不断取出序列元素,和上一轮计算结果传入函数,迭代压缩成单个值。语法如下:
reduce(function, iterable[, initializer])
参数说明:
function:必须接收 2 个参数,如 func(累计结果, 当前元素)
iterable:可迭代对象
initializer(可选):初始累积值
例如:使用 lambda 作为 function 参数
from functools import reduce
nums = [1, 2, 3, 4, 5]
# 累乘(无初始值,第一轮取前两个元素)
# a=1 x b=2 = 2
# a=2 x b=3 = 6
# a=6 x b=4 = 24
# a=24 x b=5 = 120
product = reduce(lambda a, b: a * b, nums)
print(product) # 120
# 累加(其实用 sum 更好,这里演示;设置初始值0)
# a=0 + b=1 = 1
# a=1 + b=2 = 3
# a=3 + b=3 = 6
# a=6 + b=4 = 10
# a=10 + b=5 = 15
total = reduce(lambda a, b: a + b, nums, 0)
print(total) # 15
# 找最大值
# a保存当前最大值,逐个和b比较,保留较大值
maximum = reduce(lambda a, b: a if a > b else b, nums)
print(maximum) # 5reduce 在 Python 3 被移到了 functools。 Guido 不太喜欢它,因为大多数用例可以用更清晰的 for 循环或内置函数替代。但如果你的逻辑确实是"累积"(cumulation),reduce 比手写循环更简洁。