Boltons:补上 Python 标准库缺的那些东西

Published by rcdfrd on 2025-09-22

Boltons:补上 Python 标准库缺的那些东西

1. 为什么要用 Boltons?

Python 标准库功能不少,但写代码时总会碰到一些小问题:字典不能存重复的键、装饰器会丢掉函数签名、处理嵌套数据要写一堆递归……这些问题单独拿出来都不大,但每次都要自己写代码解决,烦。

boltons 就是来填这些坑的。它是一堆纯 Python 写的小工具,没有外部依赖,想用哪个导入哪个。目前有 230 多个工具,覆盖数据结构、函数式编程、字符串处理等方面。

1.1 定位:标准库的补丁包

boltons 不是要取代标准库,而是补上标准库没覆盖到的地方。

举个例子。Web 开发中经常要处理 URL 查询参数 ?tag=python&tag=web,同一个 tag 出现了两次。标准字典只能存一个值,后面的会覆盖前面的。boltons.dictutils.OrderedMultiDict 就是专门处理这种情况的——既能存多个值,又能保持顺序。

再比如 functools.partial 用在类方法上会出问题,因为它不会自动绑定 selfboltons.funcutils.InstancePartial 修复了这个行为。

这些工具的共同点是:你可以按需导入,不用担心引入一堆依赖。项目只需要 OrderedMultiDict?那就只导入这一个。

1.2 三个优点

  • 纯 Python:跨平台,源码可读,想改就改。
  • 零依赖pip install boltons 就完事了,不会和项目里其他库冲突。
  • 模块化:每个工具都是独立的,按需取用。

1.3 适合什么场景?

  • 处理 HTTP 请求里的重复参数
  • 清洗嵌套的 JSON 数据(比如删掉所有 None 值)
  • 分块处理大文件,避免内存爆掉
  • 给计算量大的函数加缓存
  • 解析 "1,3,5-7" 这种字符串变成 [1,3,5,6,7]

2. 安装和基本用法

2.1 安装

pip install boltons

macOS 用 MacPorts 的话:

sudo port install py-boltons

2.2 导入方式

模块名都以 utils 结尾,从对应模块导入需要的工具:

from boltons.dictutils import OrderedMultiDict
from boltons.funcutils import InstancePartial
from boltons.iterutils import remap

2.3 两个简单例子

解析数字范围字符串:

from boltons.strutils import parse_int_list

cpu_cores = parse_int_list('0,7,21-22,48,55,69-70')
print(cpu_cores)  # [0, 7, 21, 22, 48, 55, 69, 70]

遍历日期范围:

from datetime import date
from boltons.timeutils import daterange

for day in daterange(date(2023, 1, 1), date(2023, 1, 10)):
    print(day)

3. dictutils 模块

3.1 OrderedMultiDict:能存重复键的字典

标准字典从 Python 3.7 开始保证插入顺序,但一个键还是只能对应一个值。OrderedMultiDict 解决了这个限制。

基本操作:

from boltons.dictutils import OrderedMultiDict

omd = OrderedMultiDict()
omd.add('tag', 'python')
omd.add('tag', 'web')
omd.add('sort', 'date')

print(omd.getlist('tag'))  # ['python', 'web']
print(omd.get('tag'))      # 'web'(最后一个)

add() 不会覆盖已有值,而是追加。getlist() 拿所有值,get() 拿最后一个。

迭代时的 multi 参数:

# 默认只返回每个键的最后一个值
for k, v in omd.items():
    print(k, v)
# tag web
# sort date

# multi=True 返回所有键值对
for k, v in omd.items(multi=True):
    print(k, v)
# tag python
# tag web
# sort date

实现原理

内部用了哈希表加双向链表。哈希表保证 O(1) 查找,链表维护插入顺序。每次 add() 在链表尾部插入新节点,同时更新哈希表里的映射。

适用场景

  • 解析 HTTP 请求头(Accept-Language 可能有多个值)
  • 处理表单的多选框
  • 解析 URL 查询参数

3.2 IndexedSet:有序去重集合

标准 set 去重但无序。IndexedSet 两个都要——去重的同时保持插入顺序。

from boltons.setutils import IndexedSet

data = IndexedSet([3, 4, 5, 0, 1, 3])  # 3 重复,会被忽略
print(data)  # IndexedSet([3, 4, 5, 0, 1])

data.add(3)  # 已存在,不会改变
print(data)  # IndexedSet([3, 4, 5, 0, 1])

# 支持标准集合运算:
set1 = IndexedSet([1, 3, 5])
set2 = IndexedSet([2, 3, 4])
print(set1 | set2)  # IndexedSet([1, 3, 5, 2, 4])

内部用字典记录元素到索引的映射,用列表按顺序存储元素。添加时先查字典判断是否存在,不存在就追加到列表末尾。

适用场景

  • 用户输入的标签去重(保持输入顺序)
  • 任务队列(不重复、按添加顺序执行)
  • 日志分析提取唯一 IP

4. funcutils 模块

4.1 wraps:保留函数签名的装饰器

标准库的 functools.wraps 会复制 __name____doc__ 等属性,但函数签名会丢失。用 inspect.signature() 看装饰后的函数,只能看到 (*args, **kwargs)

boltons.funcutils.wraps 修复了这个问题:

import time
import inspect
from boltons.funcutils import wraps

def log_time(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        print(f"{func.__name__} took {time.time() - start:.4f}s")
        return result
    return wrapper

@log_time
def calculate(a, b, multiplier=1):
    """计算 (a + b) * multiplier"""
    time.sleep(0.1)
    return (a + b) * multiplier

print(inspect.signature(calculate))  # (a, b, multiplier=1)
print(calculate.__doc__)             # 计算 (a + b) * multiplier

原理是用 FunctionBuilder 动态生成一个新函数,签名和原函数一致,函数体调用 wrapper。

高级用法:injected 和 expected

injected 从签名中隐藏参数(用于依赖注入):

from boltons.funcutils import update_wrapper

def inject_db(func):
    def wrapper(*args, **kwargs):
        kwargs['db_conn'] = get_db_connection()
        return func(*args, **kwargs)
    return update_wrapper(wrapper, func, injected=['db_conn'])

@inject_db
def query_user(user_id, db_conn):
    return db_conn.query(user_id)

# 调用时不需要传 db_conn
query_user(123)

expected 向签名中添加参数:

def with_timeout(func):
    def wrapper(*args, timeout=30, **kwargs):
        # 带超时执行
        return func(*args, **kwargs)
    return update_wrapper(wrapper, func, expected=[('timeout', 30)])

4.2 InstancePartial:修复 partial 在类方法中的问题

functools.partial 用在类方法上会出错,因为它不是描述符,不会自动绑定 self

from functools import partial

class Processor:
    def log(self, msg, level='INFO'):
        print(f"[{level}] {msg}")

    log_debug = partial(log, level='DEBUG')  # 这样写不行

p = Processor()
p.log_debug("test")  # TypeError: log() missing required argument: 'self'

InstancePartial 通过实现 __get__ 方法解决了这个问题:

from boltons.funcutils import InstancePartial

class Processor:
    def log(self, msg, level='INFO'):
        print(f"[{level}] {msg}")

    log_debug = InstancePartial(log, level='DEBUG')

p = Processor()
p.log_debug("test")  # [DEBUG] test

CachedInstancePartial 在第一次访问时缓存绑定结果,性能更好。funcutils.partial 默认就是 CachedInstancePartial

4.3 FunctionBuilder:动态构建函数

如果需要在运行时创建函数,FunctionBuilderexec 更安全、更可控:

from boltons.funcutils import FunctionBuilder

fb = FunctionBuilder(
    name='add',
    args=['a', 'b'],
    body='return a + b',
    doc='返回 a + b'
)

add = fb.get_func()
print(add(1, 2))  # 3
print(add.__doc__)  # 返回 a + b

内部原理是拼接出函数定义的源码字符串,然后用 compile() 编译。

适用场景包括:根据配置文件生成数据访问函数、动态注册 API 路由、实现 DSL 等。

5. 其他模块

5.1 iterutils

remap:递归处理嵌套结构

from boltons.iterutils import remap

data = {
    'users': [
        {'id': 1, 'name': 'Alice', 'deleted': None},
        {'id': 2, 'name': 'Bob', 'deleted': False},
    ],
    'meta': {'version': '1.0', 'deprecated': None}
}

# 删除所有 None 值
cleaned = remap(data, visit=lambda p, k, v: (k, v) if v is not None else False)

visit 函数接收路径、键、值三个参数,返回 (key, value) 保留,返回 False 删除。

research:递归查找

from boltons.iterutils import research

root = {'a': {'b': 1, 'c': (2, 'd', 3)}, 'e': None}

# 找出所有整数
results = research(root, query=lambda p, k, v: isinstance(v, int))
for path, value in results:
    print(f"{path}: {value}")
# ('a', 'b'): 1
# ('a', 'c', 0): 2
# ('a', 'c', 2): 3

5.2 strutils

slugify:生成 URL 友好字符串

from boltons.strutils import slugify

title = "Python's Best-Kept Secrets!"
print(slugify(title))  # pythons-best-kept-secrets

strip_ansi:去掉终端颜色代码

from boltons.strutils import strip_ansi

colored = "\033[31mred text\033[0m normal"
print(strip_ansi(colored))  # red text normal

5.3 timeutils

decimal_relative_time:人性化时间显示

from datetime import datetime, timedelta
from boltons.timeutils import decimal_relative_time

now = datetime.now()
print(decimal_relative_time(now - timedelta(minutes=5), now))  # 5 minutes ago
print(decimal_relative_time(now + timedelta(days=2), now))     # 2 days from now

6. 和其他库的对比

6.1 vs collections

场景 collections boltons
保持顺序的字典 OrderedDict 同样支持
一键多值 不支持 OrderedMultiDict
有序去重集合 不支持 IndexedSet

如果只需要保持顺序,OrderedDict 够用。需要一键多值时用 OrderedMultiDict

6.2 vs functools

  • functools.wraps 不保留签名,boltons.funcutils.wraps 保留。
  • functools.partial 不能用在类方法上,InstancePartial 可以。

6.3 vs toolz / more-itertools

toolzmore-itertools 偏向函数式编程风格,支持惰性求值和函数组合。

boltons 更像标准库的补丁,工具更直接,上手更快。

三个库可以一起用,根据具体需求选择。

7. 实战案例

7.1 处理 Web 请求的多值参数

from flask import Flask, request, jsonify
from boltons.dictutils import OrderedMultiDict

app = Flask(__name__)

PRODUCTS = [
    {"id": 1, "name": "Laptop", "tags": ["electronics", "computers"]},
    {"id": 2, "name": "Phone", "tags": ["electronics", "on-sale"]},
]

@app.route('/api/products')
def get_products():
    params = OrderedMultiDict(request.args)
    filter_tags = params.getlist('tag')

    if not filter_tags:
        return jsonify(PRODUCTS)

    filtered = [
        p for p in PRODUCTS
        if all(tag in p['tags'] for tag in filter_tags)
    ]
    return jsonify(filtered)

请求 /api/products?tag=electronics&tag=on-sale 会过滤出同时包含两个标签的商品。

7.2 动态添加类方法

from boltons.funcutils import CachedInstancePartial

class DataProcessor:
    def __init__(self):
        self.logs = []

    def log(self, message, level="INFO"):
        self.logs.append(f"[{level}] {message}")

class LoggingPlugin:
    def __init__(self, processor, level="DEBUG"):
        processor.log_step = CachedInstancePartial(processor.log, level=level)

processor = DataProcessor()
LoggingPlugin(processor, level="DEBUG")
processor.log_step("processing started")
print(processor.logs)  # ['[DEBUG] processing started']

7.3 带签名保留的日志装饰器

import time
import inspect
from boltons.funcutils import wraps

def log_call(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"calling {func.__name__}")
        start = time.time()
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned in {time.time()-start:.4f}s")
        return result
    return wrapper

@log_call
def calculate(a, b, multiplier=1):
    """计算 (a+b)*multiplier"""
    return (a + b) * multiplier

# 签名保留完整
print(inspect.signature(calculate))  # (a, b, multiplier=1)

8. 总结

Boltons 的价值在于:

  1. 省事——常见问题有现成工具,不用自己写

  2. 可读——remap(data, ...) 比手写递归清晰

  3. 可靠——测试覆盖好,比自己写的轮子少 bug

    项目地址:https://github.com/mahmoud/boltons