Python functools.partial:预先绑定参数,快速构造新函数

2026-09-07 00:00    #Python   #functools   #函数式编程   #算法竞赛  

有些函数本身已经完成了需要的工作,只是每次调用时都有几个参数固定不变。与其反复写 lambda 或包装函数,可以使用 functools.partial 预先绑定这些参数,得到一个新的可调用对象。

1from functools import partial
2
3parse_binary = partial(int, base=2)
4
5assert parse_binary("1010") == 10
6assert parse_binary("1111") == 15

int(text, base=2)base 被固定为 2,因此 parse_binary 只需要接收待解析的字符串。

核心理解

partial 不会立即调用原函数。它保存“原函数 + 已绑定参数”,返回一个以后可以继续接收剩余参数的新函数对象。

基本语法

1from functools import partial
2
3new_function = partial(function, *bound_args, **bound_keywords)

以后调用:

1new_function(*args, **keywords)

可以近似理解为:

1def rough_partial(function, *bound_args, **bound_keywords):
2    def new_function(*args, **keywords):
3        merged_keywords = {**bound_keywords, **keywords}
4        return function(*bound_args, *args, **merged_keywords)
5
6    return new_function

真正的 partial 是标准库实现,还支持属性检查、序列化、弱引用及 Python 3.14 的位置占位符。上面的代码只用于理解参数如何合并。

先绑定左侧位置参数

定义一个三参数函数:

1def affine(x, scale, offset):
2    return x * scale + offset

直接绑定第一个参数:

1from functools import partial
2
3start_from_ten = partial(affine, 10)
4
5assert start_from_ten(3, 2) == 32

实际调用等价于:

1affine(10, 3, 2)

新传入的位置参数会追加在已绑定位置参数的后面。因此普通 partial 最自然地绑定左侧参数。

再绑定两个位置参数:

1triple_ten_plus = partial(affine, 10, 3)
2
3assert triple_ten_plus(2) == 32

用关键字绑定指定参数

关键字参数不受位置顺序限制:

1triple = partial(affine, scale=3, offset=0)
2
3assert triple(7) == 21

也可以只固定偏移量:

1add_five_after_scaling = partial(affine, offset=5)
2
3assert add_five_after_scaling(4, 3) == 17

这次调用等价于:

1affine(4, 3, offset=5)

调用时可以覆盖已绑定关键字

调用 partial 对象时,新关键字会覆盖旧关键字:

1transform = partial(affine, scale=2, offset=1)
2
3assert transform(10) == 21
4assert transform(10, offset=100) == 120

但已绑定的位置参数不能用新的位置参数“挤掉”。如果参数可能经常被覆盖,关键字绑定通常更灵活。

一个完整参数合并例子

1def record(a, b, c=0):
2    return a, b, c
3
4
5saved = partial(record, 10, c=30)
6
7assert saved(20) == (10, 20, 30)
8assert saved(20, c=40) == (10, 20, 40)

绑定后的含义是:

1a = 10       已绑定位置参数
2b = ?        等待调用时提供
3c = 30       已绑定关键字,但允许覆盖

查看 partial 保存了什么

partial 对象保留三个重要属性:

1assert saved.func is record
2assert saved.args == (10,)
3assert saved.keywords == {"c": 30}

调试时可以直接检查这些属性,确认是否绑错了参数。

inspect.signature 还能看到剩余调用接口:

1from inspect import signature
2
3assert str(signature(saved)) == "(b, *, c=30)"

这表示新函数还需要参数 b,而 c 已有默认值 30

OJ 用法一:固定转换函数

map 的第一个参数经常固定为 int

1to_ints = partial(map, int)
2
3numbers = to_ints(["10", "20", "30"])
4
5assert list(numbers) == [10, 20, 30]

to_ints(words) 等价于 map(int, words)

注意,map 返回一次性迭代器,不是列表:

1numbers = to_ints(["1", "2", "3"])
2
3assert sum(numbers) == 6
4assert sum(numbers) == 0

需要下标、长度或重复遍历时再转成列表:

1values = list(to_ints(["1", "2", "3"]))
2
3assert values[0] == 1
4assert len(values) == 3

OJ 用法二:固定排序规则

按二元组第二项排序:

1from operator import itemgetter
2
3sort_by_second = partial(sorted, key=itemgetter(1))
4
5items = [("A", 5), ("B", 2), ("C", 7)]
6
7assert sort_by_second(items) == [("B", 2), ("A", 5), ("C", 7)]

需要降序时可以覆盖或追加 reverse

1assert sort_by_second(items, reverse=True) == [
2    ("C", 7),
3    ("A", 5),
4    ("B", 2),
5]

如果同一个排序规则会在多组小数据上反复使用,命名后的 sort_by_second 比重复写 key=itemgetter(1) 更短。

OJ 用法三:固定网格大小

 1def in_bounds(position, rows, columns):
 2    row, column = position
 3    return 0 <= row < rows and 0 <= column < columns
 4
 5
 6inside_board = partial(in_bounds, rows=5, columns=7)
 7
 8assert inside_board((0, 0))
 9assert inside_board((4, 6))
10assert not inside_board((5, 6))

rowscolumns 固定后,新函数只接收位置。它适合传给 filterallany 或原型管道:

1positions = [(0, 0), (4, 6), (5, 6), (-1, 3)]
2valid_positions = list(filter(inside_board, positions))
3
4assert valid_positions == [(0, 0), (4, 6)]

正式 BFS 的热点循环中,直接写边界条件可能更快;Python 原型阶段可以优先考虑表达是否清楚。

配合 flow 构造一元步骤

flow 要求每一步接收一个输入。partial 可以把多参数函数变成一元函数:

1def flow(value, *steps):
2    for step in steps:
3        value = step(value)
4    return value
 1input_text = "5 3 8 3 1 9 2"
 2
 3result = flow(
 4    input_text,
 5    str.split,
 6    partial(map, int),
 7    set,
 8    sorted,
 9)
10
11assert result == [1, 2, 3, 5, 8, 9]

这里 partial(map, int) 把“使用 int 映射某个 iterable”封装为一个等待 iterable 的步骤。

如果还要取前三个,可以用 lambda,因为切片的参数位置不适合旧版 partial

1result = flow(result, lambda values: values[:3])
2
3assert result == [1, 2, 3]

完整的原型、短路和随机差分工作流见Python flow:快速搭建 OJ 思路原型与随机验证

partial 与 lambda 的选择

两者都能构造新函数:

1parse_with_partial = partial(int, base=2)
2parse_with_lambda = lambda text: int(text, base=2)
3
4assert parse_with_partial("101") == parse_with_lambda("101") == 5

适合 partial

适合 lambda

例如固定除数,把被除数留给调用时传入:

1divide_by_ten = lambda numerator: numerator / 10
2
3assert divide_by_ten(40) == 4

如果写成 partial(operator.truediv, 10),得到的是 10 / x,不是 x / 10。普通 partial 只能从左边绑定位置参数,不能把第二个位置固定后空出第一个位置。Python 3.14 的 Placeholder 扩展了这部分能力,后文会讲。

partial 与 def 的选择

逻辑开始包含校验、分支、多个语句或有意义的文档时,使用 def

1def parse_nonnegative_binary(text):
2    value = int(text, base=2)
3    if value < 0:
4        raise ValueError("expected a nonnegative value")
5    return value

选择顺序可以记成:

1只绑定参数 -> partial
2一个短表达式 -> lambda
3有分支、断言或多步逻辑 -> def

partial 的目标不是消灭 def,而是避免没有新逻辑的转发函数。

调试时注意函数名字

普通函数有 __name__,但 partial 对象不会自动得到一个描述业务含义的名字:

1parse_binary = partial(int, base=2)
2
3assert not hasattr(parse_binary, "__name__")

因此某些日志和追踪工具只会显示 partial。原型中可以主动加属性:

1parse_binary.__name__ = "parse_binary"
2
3assert parse_binary.__name__ == "parse_binary"

如果日志、文档或类型提示已经成为主要需求,直接写具名 def 往往更稳妥。

绑定的是对象引用,不是副本

partial 保存参数对象本身。绑定可变对象后,外部修改会影响之后的调用:

 1def take_with_config(values, config):
 2    return values[:config["limit"]]
 3
 4
 5config = {"limit": 2}
 6take_configured = partial(take_with_config, config=config)
 7
 8assert take_configured([10, 20, 30, 40]) == [10, 20]
 9
10config["limit"] = 3
11
12assert take_configured([10, 20, 30, 40]) == [10, 20, 30]

这既可以用于共享动态配置,也可能成为难以发现的状态 Bug。竞赛原型中尽量绑定整数、字符串、元组等不可变值。

Python 3.14:使用 Placeholder 跳过位置

Python 3.14 为 partial 增加了 functools.Placeholder,可以预留任意位置参数槽位。

例如 str.replace(text, old, new) 的第一个参数是待处理字符串。希望固定 oldnew、把 text 留到以后传入:

 1from functools import Placeholder
 2
 3replace_commas = partial(
 4    str.replace,
 5    Placeholder,
 6    ",",
 7    " ",
 8)
 9
10assert replace_commas("1,2,3") == "1 2 3"

调用时传入的位置参数依次填充 Placeholder。所有占位符都必须被填满,否则抛出 TypeError

多个占位符也可以一起使用:

1remove_text = partial(
2    str.replace,
3    Placeholder,
4    Placeholder,
5    "",
6)
7
8assert remove_text("banana", "a") == "bnn"

OJ 版本兼容性

Placeholder 是 Python 3.14 新功能。洛谷等 OJ 的 Python/PyPy 版本可能更旧,提交前必须检查解释器版本。

旧版本使用 lambda:

1replace_commas_legacy = lambda text: text.replace(",", " ")
2
3assert replace_commas_legacy("1,2,3") == "1 2 3"

基础 partial 很早就存在,可以放心使用;只有 Placeholder 需要特别检查版本。

类方法中使用 partialmethod

在类定义中,普通方法需要正确绑定 self。标准库提供 partialmethod

 1from functools import partialmethod
 2
 3
 4class Counter:
 5    def __init__(self):
 6        self.value = 0
 7
 8    def change(self, delta):
 9        self.value += delta
10        return self.value
11
12    increment = partialmethod(change, 1)
13    decrement = partialmethod(change, -1)
1counter = Counter()
2
3assert counter.increment() == 1
4assert counter.increment() == 2
5assert counter.decrement() == 1

partialmethod 会先让方法描述符绑定实例,再插入预设参数。普通 OJ 函数很少需要它,但写状态类、测试辅助器或本地工具时很方便。

常见错误

以为 partial 已经执行函数

1parse_binary = partial(int, base=2)

这一行只创建可调用对象,真正转换发生在:

1value = parse_binary("1010")

把剩余位置参数顺序想反

1def subtract(a, b):
2    return a - b
3
4
5subtract_from_ten = partial(subtract, 10)
6
7assert subtract_from_ten(3) == 7

它表示 10 - x,不是 x - 10。需要后者时使用:

1subtract_ten = lambda x: x - 10

或在 Python 3.14 使用 Placeholder

重复提供已经位置绑定的参数

1def add(a, b):
2    return a + b
3
4
5add_ten = partial(add, 10)
6
7assert add_ten(5) == 15

再给两个位置参数会导致参数过多,而不是覆盖已绑定的 10

忘记迭代器和副作用

partial(map, int) 返回的仍然是 map;绑定 list.sort 后返回值仍然是 Nonepartial 只绑定参数,不改变原函数的求值方式、返回类型和副作用。

为了一行代码牺牲可读性

1# 绑定层次已经难以一眼看懂时,不如写 def

partial 最适合“函数已经合适,只差固定几个参数”的情况。若读者需要先推演参数位置才能理解,具名函数更好。

速查表

目标写法
固定左侧位置参数partial(function, value)
固定指定关键字partial(function, key=value)
调用时覆盖关键字configured(x, key=new_value)
查看原函数configured.func
查看绑定位置参数configured.args
查看绑定关键字configured.keywords
查看剩余签名inspect.signature(configured)
类方法中预设参数partialmethod(method, value)
Python 3.14 预留位置partial(function, Placeholder, fixed)

总结

functools.partial 适合把一个通用函数配置成更具体的函数:

1parse_binary = partial(int, base=2)
2to_ints = partial(map, int)
3sort_by_second = partial(sorted, key=itemgetter(1))
4inside_board = partial(in_bounds, rows=5, columns=7)

使用时记住四点:

  1. 已绑定位置参数放在调用参数之前;
  2. 调用时关键字可以覆盖已绑定关键字;
  3. 参数对象按引用保存,不会自动复制;
  4. partial 不改变原函数的返回类型、副作用或复杂度。

只绑定参数时使用 partial,需要短表达式时使用 lambda,需要分支和多步逻辑时使用 def。这样既能减少重复代码,也不会让参数绑定变成新的理解负担。

高阶函数基础见Python 函数式编程三剑客partial 在数据原型中的组合方式见Python flow:快速搭建 OJ 思路原型与随机验证

参考资料