函数式编程
函数式编程是一种编程思想。它强调:
- 函数可以像普通变量一样被传递
- 函数可以作为参数传入另一个函数
- 函数也可以作为结果返回
- 尽量把计算过程拆成清晰的小函数
Python 不是纯函数式编程语言,因为 Python 允许变量、对象状态、输入输出等副作用。但 Python 支持很多函数式编程特性,比如:
- 高阶函数
map()reduce()filter()sorted()的key- 闭包
- 匿名函数
lambda - 装饰器
- 偏函数
高阶函数
函数也是对象
在 Python 中,函数本身也是对象。
例如,abs() 是 Python 内置的求绝对值函数:
print(abs(-10)) # 10
如果只写函数名,不加括号,得到的是函数对象本身:
print(abs)
# <built-in function abs>
区别是:
abs:函数对象本身abs(-10):调用函数后得到的结果
函数对象可以赋值给变量:
f = abs
print(f(-10)) # 10
这里 f 和 abs 指向同一个函数对象,所以 f(-10) 和 abs(-10) 效果一样。
函数名也是变量
函数名本质上也是一个变量,它指向某个函数对象。
例如:
def hello():
print('hello')
f = hello
f()
输出:
hello
不要把内置函数名改成别的值:
abs = 10
# abs(-10) 会报错,因为 abs 已经指向整数 10,不再指向函数
实际写代码时,不要使用 list、str、dict、sum、max、min、abs 这些名字作为普通变量名。
函数可以作为参数
如果一个函数接收另一个函数作为参数,这个函数就叫高阶函数。
def add(x, y, f):
return f(x) + f(y)
print(add(-5, 6, abs)) # 11
执行过程可以理解为:
x = -5
y = 6
f = abs
f(x) + f(y)
# abs(-5) + abs(6)
# 5 + 6
# 11
小结
- 函数可以赋值给变量
- 函数名本身也是变量
- 函数可以作为参数传入另一个函数
- 接收函数作为参数的函数,称为高阶函数
map/reduce
map()
map() 接收两个参数:
map(函数, 可迭代对象)
它会把函数依次作用到可迭代对象的每一个元素上,并返回一个新的迭代器。
例如,把列表里的每个数字平方:
def square(x):
return x * x
result = map(square, [1, 2, 3, 4, 5])
print(result)
print(list(result))
输出:
<map object at ...>
[1, 4, 9, 16, 25]
注意:map() 返回的是迭代器,不是列表。如果想一次性看到结果,需要用 list() 转换。
普通循环写法:
result = []
for x in [1, 2, 3, 4, 5]:
result.append(square(x))
print(result)
map() 写法:
print(list(map(square, [1, 2, 3, 4, 5])))
把数字转换成字符串:
print(list(map(str, [1, 2, 3, 4, 5])))
# ['1', '2', '3', '4', '5']
reduce()
reduce() 在 functools 模块中,需要先导入:
from functools import reduce
reduce() 接收两个参数:
reduce(函数, 序列)
这个函数必须接收两个参数。reduce() 会把上一次的计算结果和下一个元素继续计算。
例如:
from functools import reduce
def add(x, y):
return x + y
print(reduce(add, [1, 3, 5, 7, 9])) # 25
执行过程:
add(1, 3) -> 4
add(4, 5) -> 9
add(9, 7) -> 16
add(16, 9) -> 25
也就是:
reduce(add, [1, 3, 5, 7, 9])
# 等价于 add(add(add(add(1, 3), 5), 7), 9)
用 reduce 把数字列表变成整数
from functools import reduce
def fn(x, y):
return x * 10 + y
print(reduce(fn, [1, 3, 5, 7, 9])) # 13579
执行过程:
1 * 10 + 3 -> 13
13 * 10 + 5 -> 135
135 * 10 + 7 -> 1357
1357 * 10 + 9 -> 13579
用 map 和 reduce 实现 str2int
from functools import reduce
DIGITS = {
'0': 0,
'1': 1,
'2': 2,
'3': 3,
'4': 4,
'5': 5,
'6': 6,
'7': 7,
'8': 8,
'9': 9
}
def char2num(ch):
return DIGITS[ch]
def str2int(s):
return reduce(lambda x, y: x * 10 + y, map(char2num, s))
print(str2int('13579')) # 13579
这里:
map(char2num, '13579')把'1'、'3'、'5'、'7'、'9'转成1、3、5、7、9reduce()再把这些数字累积成整数13579
练习 1:规范英文名字
利用 map(),把用户输入的不规范英文名字变成首字母大写、其他小写的规范名字。
def normalize(name):
return name[0].upper() + name[1:].lower()
L1 = ['adam', 'LISA', 'barT']
L2 = list(map(normalize, L1))
print(L2)
输出:
['Adam', 'Lisa', 'Bart']
也可以使用字符串方法 capitalize():
def normalize(name):
return name.capitalize()
练习 2:用 reduce 求积
from functools import reduce
def prod(L):
return reduce(lambda x, y: x * y, L)
print('3 * 5 * 7 * 9 =', prod([3, 5, 7, 9]))
if prod([3, 5, 7, 9]) == 945:
print('测试成功!')
else:
print('测试失败!')
练习 3:str2float
利用 map() 和 reduce() 编写一个 str2float() 函数,把字符串 '123.456' 转换成浮点数 123.456。
from functools import reduce
DIGITS = {
'0': 0,
'1': 1,
'2': 2,
'3': 3,
'4': 4,
'5': 5,
'6': 6,
'7': 7,
'8': 8,
'9': 9
}
def char2num(ch):
return DIGITS[ch]
def str2int(s):
return reduce(lambda x, y: x * 10 + y, map(char2num, s), 0)
def str2float(s):
parts = s.split('.')
if len(parts) == 1:
return float(str2int(parts[0]))
integer_part = str2int(parts[0])
decimal_part = str2int(parts[1])
return integer_part + decimal_part / (10 ** len(parts[1]))
print('str2float("123.456") =', str2float('123.456'))
if abs(str2float('123.456') - 123.456) < 0.00001:
print('测试成功!')
else:
print('测试失败!')
这里 reduce(..., 0) 的第三个参数是初始值。这样即使传入空字符串,也不会直接报 reduce() of empty iterable。
小结
map()用于把一个函数作用到每个元素上map()返回迭代器,需要list()才能一次性看到结果reduce()用于把一组元素累积成一个结果reduce()的函数参数必须接收两个参数- 简单转换也可以使用列表生成式,复杂函数组合时
map/reduce更能体现抽象
filter
知识点
filter() 用于过滤序列。
基本形式:
filter(判断函数, 可迭代对象)
判断函数会作用到每个元素上:
- 返回
True,保留该元素 - 返回
False,丢弃该元素
保留奇数:
def is_odd(n):
return n % 2 == 1
result = filter(is_odd, [1, 2, 4, 5, 6, 9, 10, 15])
print(list(result))
输出:
[1, 5, 9, 15]
过滤空字符串:
def not_empty(s):
return s and s.strip()
print(list(filter(not_empty, ['A', '', 'B', None, 'C', ' '])))
# ['A', 'B', 'C']
注意:filter() 返回的也是迭代器。
用 filter 求素数
埃氏筛法的大致思路是:
- 从
2开始,2是素数 - 去掉所有
2的倍数 - 剩下序列中的第一个数是
3,它是素数 - 去掉所有
3的倍数 - 不断重复
先构造从 3 开始的奇数生成器:
def _odd_iter():
n = 1
while True:
n = n + 2
yield n
定义筛选函数:
def _not_divisible(n):
return lambda x: x % n > 0
定义素数生成器:
def primes():
yield 2
it = _odd_iter()
while True:
n = next(it)
yield n
it = filter(_not_divisible(n), it)
打印 100 以内的素数:
for n in primes():
if n < 100:
print(n)
else:
break
这里 primes() 是无限生成器,所以使用时必须设置停止条件。
练习:筛选回数
回数是从左向右读和从右向左读都一样的数,例如 12321、909。
def is_palindrome(n):
s = str(n)
return s == s[::-1]
output = filter(is_palindrome, range(1, 1000))
print('1~1000:', list(output))
if list(filter(is_palindrome, range(1, 200))) == [
1, 2, 3, 4, 5, 6, 7, 8, 9,
11, 22, 33, 44, 55, 66, 77, 88, 99,
101, 111, 121, 131, 141, 151, 161, 171, 181, 191
]:
print('测试成功!')
else:
print('测试失败!')
小结
filter()用于筛选元素- 筛选函数返回真值时保留元素,返回假值时丢弃元素
filter()返回迭代器,只有真正取值时才会计算
sorted
知识点
sorted() 用于排序,并返回一个新的列表。
print(sorted([36, 5, -12, 9, -21]))
# [-21, -12, 5, 9, 36]
sorted() 不会修改原列表:
nums = [3, 1, 2]
result = sorted(nums)
print(nums) # [3, 1, 2]
print(result) # [1, 2, 3]
key 参数
sorted() 也是高阶函数,因为它可以接收一个 key 函数。
按绝对值排序:
print(sorted([36, 5, -12, 9, -21], key=abs))
# [5, 9, -12, -21, 36]
可以这样理解:
原始值: 36 5 -12 9 -21
key值: 36 5 12 9 21
排序后: 5 9 -12 -21 36
默认字符串排序会区分大小写:
print(sorted(['bob', 'about', 'Zoo', 'Credit']))
# ['Credit', 'Zoo', 'about', 'bob']
如果想忽略大小写,可以使用 key=str.lower:
print(sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower))
# ['about', 'bob', 'Credit', 'Zoo']
反向排序使用 reverse=True:
print(sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower, reverse=True))
# ['Zoo', 'Credit', 'bob', 'about']
练习:学生排序
L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)]
def by_name(t):
return t[0]
def by_score(t):
return t[1]
print(sorted(L, key=by_name))
print(sorted(L, key=by_score, reverse=True))
输出:
[('Adam', 92), ('Bart', 66), ('Bob', 75), ('Lisa', 88)]
[('Adam', 92), ('Lisa', 88), ('Bob', 75), ('Bart', 66)]
小结
sorted()返回新列表,不修改原列表key函数用于告诉 Python “按什么规则排序”reverse=True用于反向排序- 排序复杂对象时,通常通过
key提取排序依据
返回函数
知识点
高阶函数不仅可以接收函数作为参数,也可以把函数作为结果返回。
普通求和函数:
def calc_sum(*args):
total = 0
for n in args:
total = total + n
return total
print(calc_sum(1, 3, 5, 7, 9)) # 25
如果不想立刻求和,而是想稍后再算,可以返回一个函数:
def lazy_sum(*args):
def do_sum():
total = 0
for n in args:
total = total + n
return total
return do_sum
f = lazy_sum(1, 3, 5, 7, 9)
print(f) # <function lazy_sum.<locals>.do_sum at ...>
print(f()) # 25
调用 lazy_sum() 时,并不会立刻求和,而是返回内部函数 do_sum。真正执行 f() 时,才开始求和。
闭包
内部函数引用了外部函数的变量,这种结构叫闭包。
在上面的例子中:
- 外部函数是
lazy_sum - 内部函数是
do_sum - 内部函数使用了外部函数的参数
args - 即使
lazy_sum()执行结束了,args仍然被do_sum保存着
每次调用外部函数,都会得到一个新的函数:
f1 = lazy_sum(1, 3, 5, 7, 9)
f2 = lazy_sum(1, 3, 5, 7, 9)
print(f1 == f2) # False
print(f1()) # 25
print(f2()) # 25
闭包中的循环变量问题
看下面的代码:
def count():
fs = []
for i in range(1, 4):
def f():
return i * i
fs.append(f)
return fs
f1, f2, f3 = count()
print(f1())
print(f2())
print(f3())
你可能以为结果是:
1
4
9
实际结果是:
9
9
9
原因是:返回的函数没有立刻执行。等到 f1()、f2()、f3() 真正执行时,循环早就结束了,此时 i 的值已经是 3。
解决方法:用另一个函数把当前的 i 绑定住。
def count():
def make_func(j):
def f():
return j * j
return f
fs = []
for i in range(1, 4):
fs.append(make_func(i))
return fs
f1, f2, f3 = count()
print(f1()) # 1
print(f2()) # 4
print(f3()) # 9
nonlocal
如果内部函数只是读取外部变量,不需要 nonlocal:
def inc():
x = 0
def fn():
return x + 1
return fn
f = inc()
print(f()) # 1
print(f()) # 1
如果内部函数要修改外部变量,需要使用 nonlocal:
def inc():
x = 0
def fn():
nonlocal x
x = x + 1
return x
return fn
f = inc()
print(f()) # 1
print(f()) # 2
print(f()) # 3
如果不写 nonlocal x,Python 会把 x = x + 1 中的 x 当作内部函数自己的局部变量,从而报错。
练习:计数器
利用闭包返回一个计数器函数,每次调用它返回递增整数。
def createCounter():
count = 0
def counter():
nonlocal count
count = count + 1
return count
return counter
counterA = createCounter()
print(counterA(), counterA(), counterA(), counterA(), counterA())
counterB = createCounter()
if [counterB(), counterB(), counterB(), counterB()] == [1, 2, 3, 4]:
print('测试通过!')
else:
print('测试失败!')
小结
- 函数可以返回另一个函数
- 返回函数时,内部函数不会立刻执行
- 闭包可以保存外部函数的变量
- 闭包中不要直接引用会变化的循环变量
- 内部函数要修改外部变量时,需要使用
nonlocal
匿名函数
知识点
匿名函数使用 lambda 表达式定义。
普通函数:
def square(x):
return x * x
匿名函数:
lambda x: x * x
配合 map() 使用:
print(list(map(lambda x: x * x, [1, 2, 3, 4, 5])))
# [1, 4, 9, 16, 25]
lambda 的基本形式:
lambda 参数: 表达式
特点:
- 没有函数名
- 只能写一个表达式
- 表达式的结果就是返回值
- 适合非常短的小函数
匿名函数也可以赋值给变量:
f = lambda x: x * x
print(f(5)) # 25
不过,如果函数逻辑稍微复杂一些,更推荐使用 def,可读性更好。
练习
请用匿名函数改造下面的代码:
def is_odd(n):
return n % 2 == 1
L = list(filter(is_odd, range(1, 20)))
print(L)
参考解答:
L = list(filter(lambda n: n % 2 == 1, range(1, 20)))
print(L)
小结
lambda用于定义简单匿名函数lambda只能写一个表达式- 简单的一次性函数适合用
lambda - 复杂逻辑更适合用
def
装饰器
装饰器是本章最容易绕的内容之一。先记住一句话:
装饰器本质上是一个函数,它接收一个函数作为参数,并返回一个新的函数。
它的作用是:在不修改原函数代码的前提下,给原函数增加功能。
为什么需要装饰器
假设有一个函数:
def now():
print('2026-05-15')
调用:
now()
输出:
2026-05-15
现在希望每次调用 now() 前都打印日志:
call now():
2026-05-15
最直接的办法是改原函数:
def now():
print('call now():')
print('2026-05-15')
但如果有很多函数都要加日志,就要到处改代码。装饰器就是为了解决这种“统一增强函数功能”的问题。
第一步:函数可以赋值
函数对象可以赋值给变量:
def now():
print('2026-05-15')
f = now
f()
函数对象还有 __name__ 属性:
print(now.__name__) # now
print(f.__name__) # now
第二步:手动包装一个函数
先不写装饰器语法,只写一个普通包装函数:
def now():
print('2026-05-15')
def wrapper():
print('call now():')
now()
wrapper()
输出:
call now():
2026-05-15
这个写法能增强 now(),但问题是写死了,只能包装 now。
第三步:把被包装函数作为参数传进来
def now():
print('2026-05-15')
def log(func):
def wrapper():
print('call %s():' % func.__name__)
return func()
return wrapper
new_now = log(now)
new_now()
输出:
call now():
2026-05-15
这里发生了几件事:
log(now)把原函数now传给loglog()内部定义了一个新函数wrapperwrapper里面先打印日志,再调用原函数funclog()返回wrappernew_now()实际调用的是wrapper()
所以:
new_now = log(now)
意思是:
用 log 包装 now,得到一个增强后的新函数
第四步:让原来的函数名指向包装后的函数
可以直接写:
now = log(now)
now()
此时:
- 右边的
now是原函数 log(now)返回wrapper- 左边的
now重新指向wrapper
所以之后调用 now(),执行的是 wrapper()。
第五步:使用 @ 语法
Python 提供了更简洁的写法:
def log(func):
def wrapper():
print('call %s():' % func.__name__)
return func()
return wrapper
@log
def now():
print('2026-05-15')
now()
这段代码等价于:
def now():
print('2026-05-15')
now = log(now)
重点:@log 不是调用 now(),而是在函数定义完成后,立刻执行:
now = log(now)
支持任意参数的装饰器
上面的 wrapper() 只能包装无参数函数。如果原函数有参数,就会出问题。
例如:
def add(x, y):
return x + y
更通用的写法是使用 *args 和 **kw:
def log(func):
def wrapper(*args, **kw):
print('call %s():' % func.__name__)
return func(*args, **kw)
return wrapper
@log
def add(x, y):
return x + y
print(add(3, 5)) # 8
这里:
*args接收任意位置参数**kw接收任意关键字参数func(*args, **kw)把参数原样转交给原函数
这就是为什么装饰器里的 wrapper 经常写成:
def wrapper(*args, **kw):
...
使用 functools.wraps 保留原函数信息
看下面代码:
def log(func):
def wrapper(*args, **kw):
print('call %s():' % func.__name__)
return func(*args, **kw)
return wrapper
@log
def now():
print('2026-05-15')
print(now.__name__)
输出是:
wrapper
因为现在的 now 实际上已经指向 wrapper 了。
但我们通常希望它仍然表现得像原来的 now 函数。这时要使用 functools.wraps:
import functools
def log(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print('call %s():' % func.__name__)
return func(*args, **kw)
return wrapper
@log
def now():
print('2026-05-15')
print(now.__name__) # now
记忆方式:写装饰器时,几乎总是在 wrapper 前面加:
@functools.wraps(func)
带参数的装饰器
前面的装饰器用法是:
@log
def now():
pass
如果希望装饰器本身能接收参数,例如:
@log('execute')
def now():
pass
就需要再多包一层函数。
先看完整代码:
import functools
def log(text):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print('%s %s():' % (text, func.__name__))
return func(*args, **kw)
return wrapper
return decorator
@log('execute')
def now():
print('2026-05-15')
now()
输出:
execute now():
2026-05-15
这三层分别负责:
log(text):接收装饰器自己的参数,比如'execute'decorator(func):接收被装饰的原函数wrapper(*args, **kw):接收原函数调用时传入的参数,并真正执行增强逻辑
也就是说:
@log('execute')
def now():
print('2026-05-15')
等价于:
def now():
print('2026-05-15')
now = log('execute')(now)
拆开看:
decorator = log('execute')
now = decorator(now)
再继续拆:
log('execute') 返回 decorator
decorator(now) 返回 wrapper
now 最终指向 wrapper
对比:无参数装饰器和带参数装饰器
无参数装饰器:
@log
def f():
pass
等价于:
f = log(f)
带参数装饰器:
@log('execute')
def f():
pass
等价于:
f = log('execute')(f)
所以层数不同:
无参数装饰器:装饰器函数 -> wrapper
带参数装饰器:接收参数的函数 -> 装饰器函数 -> wrapper
装饰器什么时候执行
这是一个很容易混淆的点。
def log(func):
print('正在装饰:', func.__name__)
def wrapper(*args, **kw):
print('正在调用:', func.__name__)
return func(*args, **kw)
return wrapper
@log
def hello():
print('hello')
print('准备调用 hello')
hello()
输出顺序是:
正在装饰: hello
准备调用 hello
正在调用: hello
hello
说明:
@log在函数定义完成后立刻执行wrapper里的代码在函数被调用时执行
练习 1:统计函数执行时间
请设计一个装饰器,它可作用于任何函数上,并打印该函数的执行时间。
import functools
import time
def metric(fn):
@functools.wraps(fn)
def wrapper(*args, **kw):
start = time.time()
result = fn(*args, **kw)
end = time.time()
print('%s executed in %.2f ms' % (fn.__name__, (end - start) * 1000))
return result
return wrapper
@metric
def fast(x, y):
time.sleep(0.0012)
return x + y
@metric
def slow(x, y, z):
time.sleep(0.1234)
return x * y * z
f = fast(11, 22)
s = slow(11, 22, 33)
if f != 33:
print('测试失败!')
elif s != 7986:
print('测试失败!')
else:
print('测试成功!')
注意:统计时间应该放在 wrapper 里,因为我们要统计的是“函数调用时”的耗时,而不是“装饰时”的耗时。
练习 2:调用前后打印日志
import functools
def log(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print('begin call')
result = func(*args, **kw)
print('end call')
return result
return wrapper
@log
def hello(name):
print('hello,', name)
hello('Alice')
输出:
begin call
hello, Alice
end call
练习 3:同时支持 @log 和 @log('execute')
这个题稍难,因为 @log 和 @log('execute') 的调用方式不同。
import functools
def log(arg):
if callable(arg):
func = arg
@functools.wraps(func)
def wrapper(*args, **kw):
print('call %s():' % func.__name__)
return func(*args, **kw)
return wrapper
text = arg
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print('%s %s():' % (text, func.__name__))
return func(*args, **kw)
return wrapper
return decorator
@log
def f1():
print('f1')
@log('execute')
def f2():
print('f2')
f1()
f2()
为什么这样写?
- 使用
@log时,Python 会执行f1 = log(f1),所以传给log的是函数 - 使用
@log('execute')时,Python 会先执行log('execute'),所以传给log的是字符串 callable(arg)可以判断arg是否可调用,从而区分两种情况
常见误区
@log等价于func = log(func),不是每次调用函数时才执行log(func)wrapper里一定要return func(*args, **kw),否则原函数的返回值会丢失- 通用装饰器通常使用
*args, **kw接收参数 - 写装饰器时建议使用
functools.wraps - 带参数装饰器比普通装饰器多一层,是因为要先接收装饰器自己的参数
小结
- 装饰器用于在不修改原函数代码的情况下增强函数功能
- 装饰器本质是“接收函数,返回新函数”的高阶函数
@log等价于func = log(func)@log('execute')等价于func = log('execute')(func)wrapper是真正被调用的新函数functools.wraps用于保留原函数的元信息
偏函数
偏函数也容易让人迷糊,先记一句话:
偏函数就是固定住一个函数的部分参数,得到一个更容易调用的新函数。
这里的“偏函数”和数学里的偏函数不是一回事。
从 int() 开始理解
int() 可以把字符串转换成整数:
print(int('12345')) # 12345
默认按十进制转换。其实 int() 还有一个参数 base:
print(int('12345', base=8)) # 5349
print(int('12345', base=16)) # 74565
如果我们经常要把二进制字符串转换成整数,就要反复写:
int('1000000', base=2)
int('1010101', base=2)
这有点麻烦。
普通函数写法
我们可以自己定义一个函数,默认使用二进制:
def int2(x):
return int(x, base=2)
print(int2('1000000')) # 64
print(int2('1010101')) # 85
这就是“固定住 base=2”。
使用 functools.partial
functools.partial 可以帮我们自动创建这种函数:
import functools
int2 = functools.partial(int, base=2)
print(int2('1000000')) # 64
print(int2('1010101')) # 85
这行代码:
int2 = functools.partial(int, base=2)
大致相当于:
def int2(x):
return int(x, base=2)
不过 partial 创建出来的函数更通用,它仍然允许你临时覆盖参数:
print(int2('1000000', base=10)) # 1000000
固定关键字参数
import functools
int2 = functools.partial(int, base=2)
调用:
int2('10010')
可以理解为:
kw = {'base': 2}
int('10010', **kw)
也就是自动把 base=2 补进去。
固定位置参数
partial 不只能固定关键字参数,也可以固定位置参数。
import functools
max2 = functools.partial(max, 10)
print(max2(5, 6, 7)) # 10
print(max2(11, 6, 7)) # 11
这行代码:
max2 = functools.partial(max, 10)
表示:以后调用 max2(...) 时,都会自动把 10 放到参数最前面。
所以:
max2(5, 6, 7)
相当于:
max(10, 5, 6, 7)
partial 和默认参数的区别
普通默认参数写法:
def int2(x, base=2):
return int(x, base)
偏函数写法:
import functools
int2 = functools.partial(int, base=2)
二者都能降低调用难度,但区别是:
- 默认参数需要你自己定义一个新函数
partial可以基于已有函数快速生成新函数partial更适合“已有函数很好,只是某些参数经常固定”的场景
再看一个例子:排序偏函数
假设经常需要忽略大小写排序:
names = ['bob', 'about', 'Zoo', 'Credit']
print(sorted(names, key=str.lower))
可以创建一个偏函数:
import functools
sort_ignore_case = functools.partial(sorted, key=str.lower)
print(sort_ignore_case(['bob', 'about', 'Zoo', 'Credit']))
# ['about', 'bob', 'Credit', 'Zoo']
如果经常倒序并忽略大小写:
sort_ignore_case_desc = functools.partial(sorted, key=str.lower, reverse=True)
print(sort_ignore_case_desc(['bob', 'about', 'Zoo', 'Credit']))
# ['Zoo', 'Credit', 'bob', 'about']
什么时候用偏函数
适合使用偏函数的场景:
- 某个已有函数很常用
- 但每次调用都要重复传同样的参数
- 想生成一个更专门、更短的新函数名
例如:
int2 = functools.partial(int, base=2)
sort_desc = functools.partial(sorted, reverse=True)
print_json = functools.partial(print, sep=' | ')
常见误区
- 偏函数不会立刻执行原函数,只是生成一个新函数
- 固定位置参数时,参数会默认放到前面
- 固定关键字参数时,调用时仍然可以覆盖
- 偏函数不是为了让代码更“高级”,而是为了减少重复参数
小结
functools.partial()用于创建偏函数- 偏函数可以固定原函数的一部分参数
- 偏函数返回的是一个新的可调用对象
- 固定参数后,新函数调用起来更简单
本章综合小结
- 函数可以作为变量、参数和返回值
- 接收函数作为参数的函数叫高阶函数
map()用于映射,filter()用于筛选,reduce()用于累积sorted()的关键是理解key函数- 闭包可以保存外部函数中的变量
lambda适合短小的一次性函数- 装饰器用于在不修改原函数的前提下增强函数功能
- 偏函数用于固定函数的一部分参数,生成更易调用的新函数
练习题
下面练习按难度从低到高排列。
基础题
- 定义函数
square(x),使用map()计算[1, 2, 3, 4, 5]每个元素的平方。 - 使用
map(str, nums)把数字列表[1, 20, 300]转成字符串列表。 - 使用
filter()筛选出1~50中所有偶数。 - 使用
filter()删除列表['A', '', None, 'B', ' ', 'C']中的空值。 - 使用
sorted()对[36, 5, -12, 9, -21]按绝对值排序。
map/reduce/filter 练习
- 使用
reduce()求列表[1, 2, 3, 4, 5]的乘积。 - 编写
str2int(s),把数字字符串转换成整数,不使用int()。 - 编写
normalize(name),把英文名变成首字母大写、其余小写。 - 使用
filter()找出1~1000中所有回文数。 - 使用
map()和lambda把列表[1, 2, 3, 4]转换成[2, 4, 6, 8]。
sorted 练习
- 给定学生列表
students = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)],按姓名排序。 - 对上面的学生列表按成绩从高到低排序。
- 给定字符串列表
['bob', 'about', 'Zoo', 'Credit'],忽略大小写排序。 - 给定字典列表:
users = [
{'name': 'Bob', 'age': 20},
{'name': 'Alice', 'age': 18},
{'name': 'Tom', 'age': 22}
]
按年龄从小到大排序。
闭包和 lambda 练习
- 编写
lazy_product(*args),返回一个函数,调用该函数时才计算乘积。 - 编写
createCounter(),每次调用返回递增整数。 - 修复下面闭包代码,让结果分别为
1、4、9:
def count():
fs = []
for i in range(1, 4):
def f():
return i * i
fs.append(f)
return fs
- 使用
lambda和filter()筛选出列表中的奇数。
装饰器练习
- 编写装饰器
log,在函数调用前打印begin call,调用后打印end call。 - 编写装饰器
metric,打印任意函数的执行时间。 - 编写装饰器
debug,打印函数名、参数和返回值。 - 编写带参数装饰器
prefix_log(text),调用函数前打印自定义文本。 - 思考并实现一个装饰器
run_twice,让被装饰函数连续执行两次。
偏函数练习
- 使用
functools.partial创建int8,默认按八进制转换字符串。 - 使用
functools.partial创建sort_desc,默认倒序排序。 - 使用
functools.partial创建print_with_sep,默认使用' | '作为print()的分隔符。 - 使用
functools.partial创建忽略大小写的排序函数。 - 对比手写函数和偏函数两种方式实现
int2()。
小项目练手
项目:学生成绩分析工具
目标:写一个小型成绩分析工具,综合练习高阶函数、map()、filter()、sorted()、闭包、装饰器和偏函数。
原始数据:
students = [
{'name': ' adam ', 'score': '85', 'class': 'A'},
{'name': 'LISA', 'score': '92', 'class': 'A'},
{'name': 'barT', 'score': '66', 'class': 'B'},
{'name': 'Bob', 'score': '59', 'class': 'B'},
{'name': 'alice', 'score': '100', 'class': 'A'}
]
基础要求
- 使用
map()清洗数据:- 姓名去掉首尾空格
- 姓名变成首字母大写
- 分数字符串转换成整数
- 使用
filter()筛选及格学生,分数>= 60。 - 使用
sorted()按成绩从高到低排序。 - 使用
reduce()计算总分。 - 使用闭包创建一个计数器,统计分析函数被调用了几次。
- 使用装饰器统计某个分析函数的执行时间。
- 使用偏函数创建:
sort_by_score_descfilter_passed
推荐代码骨架
import functools
import time
from functools import reduce
students = [
{'name': ' adam ', 'score': '85', 'class': 'A'},
{'name': 'LISA', 'score': '92', 'class': 'A'},
{'name': 'barT', 'score': '66', 'class': 'B'},
{'name': 'Bob', 'score': '59', 'class': 'B'},
{'name': 'alice', 'score': '100', 'class': 'A'}
]
def metric(func):
@functools.wraps(func)
def wrapper(*args, **kw):
start = time.time()
result = func(*args, **kw)
end = time.time()
print('%s executed in %.2f ms' % (func.__name__, (end - start) * 1000))
return result
return wrapper
def create_counter():
count = 0
def counter():
nonlocal count
count = count + 1
return count
return counter
def normalize_student(student):
return {
'name': student['name'].strip().capitalize(),
'score': int(student['score']),
'class': student['class']
}
def is_passed(student):
return student['score'] >= 60
def by_score(student):
return student['score']
sort_by_score_desc = functools.partial(sorted, key=by_score, reverse=True)
@metric
def analyze(students):
cleaned = list(map(normalize_student, students))
passed = list(filter(is_passed, cleaned))
ranked = sort_by_score_desc(passed)
total = reduce(lambda x, y: x + y['score'], ranked, 0)
average = total / len(ranked) if ranked else 0
return {
'passed': passed,
'ranked': ranked,
'total': total,
'average': average
}
def main():
counter = create_counter()
result = analyze(students)
print('第', counter(), '次分析')
print('及格学生:', result['passed'])
print('成绩排名:', result['ranked'])
print('总分:', result['total'])
print('平均分:', result['average'])
if __name__ == '__main__':
main()
进阶要求
- 按班级统计平均分
- 找出每个班级的最高分学生
- 写一个带参数装饰器
@metric('分析耗时') - 写一个装饰器
@debug打印函数参数和返回值 - 用偏函数创建
top3_students,默认返回成绩前三名
项目复盘
完成后重点回顾:
- 哪些函数接收了另一个函数作为参数?
- 哪些地方用了
map()、filter()、reduce()? sorted()的key函数返回的是什么?- 装饰器中哪一层在“定义函数时”执行?哪一层在“调用函数时”执行?
- 偏函数固定了哪些参数?调用时还能不能覆盖?
浙公网安备 33010602011771号