Boltons:补上 Python 标准库缺的那些东西
Boltons:补上 Python 标准库缺的那些东西
1. 为什么要用 Boltons?
Python 标准库功能不少,但写代码时总会碰到一些小问题:字典不能存重复的键、装饰器会丢掉函数签名、处理嵌套数据要写一堆递归……这些问题单独拿出来都不大,但每次都要自己写代码解决,烦。
boltons 就是来填这些坑的。它是一堆纯 Python 写的小工具,没有外部依赖,想用哪个导入哪个。目前有 230 多个工具,覆盖数据结构、函数式编程、字符串处理等方面。
1.1 定位:标准库的补丁包
boltons 不是要取代标准库,而是补上标准库没覆盖到的地方。
举个例子。Web 开发中经常要处理 URL 查询参数 ?tag=python&tag=web,同一个 tag 出现了两次。标准字典只能存一个值,后面的会覆盖前面的。boltons.dictutils.OrderedMultiDict 就是专门处理这种情况的——既能存多个值,又能保持顺序。
再比如 functools.partial 用在类方法上会出问题,因为它不会自动绑定 self。boltons.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:动态构建函数
如果需要在运行时创建函数,FunctionBuilder 比 exec 更安全、更可控:
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
toolz 和 more-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 的价值在于:
-
省事——常见问题有现成工具,不用自己写
-
可读——
remap(data, ...)比手写递归清晰 -
可靠——测试覆盖好,比自己写的轮子少 bug
项目地址:https://github.com/mahmoud/boltons