hidewood

博客园 首页 新随笔 联系 订阅 管理

目录

函数式编程

函数式编程是一种编程思想。它强调:

  • 函数可以像普通变量一样被传递
  • 函数可以作为参数传入另一个函数
  • 函数也可以作为结果返回
  • 尽量把计算过程拆成清晰的小函数

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

这里 fabs 指向同一个函数对象,所以 f(-10)abs(-10) 效果一样。

函数名也是变量

函数名本质上也是一个变量,它指向某个函数对象。

例如:

def hello():
    print('hello')

f = hello
f()

输出:

hello

不要把内置函数名改成别的值:

abs = 10

# abs(-10) 会报错,因为 abs 已经指向整数 10,不再指向函数

实际写代码时,不要使用 liststrdictsummaxminabs 这些名字作为普通变量名。

函数可以作为参数

如果一个函数接收另一个函数作为参数,这个函数就叫高阶函数。

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' 转成 13579
  • reduce() 再把这些数字累积成整数 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 求素数

埃氏筛法的大致思路是:

  1. 2 开始,2 是素数
  2. 去掉所有 2 的倍数
  3. 剩下序列中的第一个数是 3,它是素数
  4. 去掉所有 3 的倍数
  5. 不断重复

先构造从 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() 是无限生成器,所以使用时必须设置停止条件。

练习:筛选回数

回数是从左向右读和从右向左读都一样的数,例如 12321909

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

这里发生了几件事:

  1. log(now) 把原函数 now 传给 log
  2. log() 内部定义了一个新函数 wrapper
  3. wrapper 里面先打印日志,再调用原函数 func
  4. log() 返回 wrapper
  5. new_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 适合短小的一次性函数
  • 装饰器用于在不修改原函数的前提下增强函数功能
  • 偏函数用于固定函数的一部分参数,生成更易调用的新函数

练习题

下面练习按难度从低到高排列。

基础题

  1. 定义函数 square(x),使用 map() 计算 [1, 2, 3, 4, 5] 每个元素的平方。
  2. 使用 map(str, nums) 把数字列表 [1, 20, 300] 转成字符串列表。
  3. 使用 filter() 筛选出 1~50 中所有偶数。
  4. 使用 filter() 删除列表 ['A', '', None, 'B', ' ', 'C'] 中的空值。
  5. 使用 sorted()[36, 5, -12, 9, -21] 按绝对值排序。

map/reduce/filter 练习

  1. 使用 reduce() 求列表 [1, 2, 3, 4, 5] 的乘积。
  2. 编写 str2int(s),把数字字符串转换成整数,不使用 int()
  3. 编写 normalize(name),把英文名变成首字母大写、其余小写。
  4. 使用 filter() 找出 1~1000 中所有回文数。
  5. 使用 map()lambda 把列表 [1, 2, 3, 4] 转换成 [2, 4, 6, 8]

sorted 练习

  1. 给定学生列表 students = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)],按姓名排序。
  2. 对上面的学生列表按成绩从高到低排序。
  3. 给定字符串列表 ['bob', 'about', 'Zoo', 'Credit'],忽略大小写排序。
  4. 给定字典列表:
users = [
    {'name': 'Bob', 'age': 20},
    {'name': 'Alice', 'age': 18},
    {'name': 'Tom', 'age': 22}
]

按年龄从小到大排序。

闭包和 lambda 练习

  1. 编写 lazy_product(*args),返回一个函数,调用该函数时才计算乘积。
  2. 编写 createCounter(),每次调用返回递增整数。
  3. 修复下面闭包代码,让结果分别为 149
def count():
    fs = []
    for i in range(1, 4):
        def f():
            return i * i
        fs.append(f)
    return fs
  1. 使用 lambdafilter() 筛选出列表中的奇数。

装饰器练习

  1. 编写装饰器 log,在函数调用前打印 begin call,调用后打印 end call
  2. 编写装饰器 metric,打印任意函数的执行时间。
  3. 编写装饰器 debug,打印函数名、参数和返回值。
  4. 编写带参数装饰器 prefix_log(text),调用函数前打印自定义文本。
  5. 思考并实现一个装饰器 run_twice,让被装饰函数连续执行两次。

偏函数练习

  1. 使用 functools.partial 创建 int8,默认按八进制转换字符串。
  2. 使用 functools.partial 创建 sort_desc,默认倒序排序。
  3. 使用 functools.partial 创建 print_with_sep,默认使用 ' | ' 作为 print() 的分隔符。
  4. 使用 functools.partial 创建忽略大小写的排序函数。
  5. 对比手写函数和偏函数两种方式实现 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'}
]

基础要求

  1. 使用 map() 清洗数据:
    • 姓名去掉首尾空格
    • 姓名变成首字母大写
    • 分数字符串转换成整数
  2. 使用 filter() 筛选及格学生,分数 >= 60
  3. 使用 sorted() 按成绩从高到低排序。
  4. 使用 reduce() 计算总分。
  5. 使用闭包创建一个计数器,统计分析函数被调用了几次。
  6. 使用装饰器统计某个分析函数的执行时间。
  7. 使用偏函数创建:
    • sort_by_score_desc
    • filter_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 函数返回的是什么?
  • 装饰器中哪一层在“定义函数时”执行?哪一层在“调用函数时”执行?
  • 偏函数固定了哪些参数?调用时还能不能覆盖?
posted on 2026-05-15 16:49  hidewood  阅读(42)  评论(0)    收藏  举报