Python案例:如何优雅地封装一个分页工具?——从零构建可复用分页逻辑
📚 目录导读
- 为什么需要封装分页工具? —— 避免重复造轮子
- 分页核心原理拆解 —— 页码、偏移量与总数
- 案例1:基础分页函数封装 —— 掌握最小核心逻辑
- 案例2:面向对象封装分页类 —— 提升可扩展性
- 案例3:对接数据库(MySQL/ORM)的分页封装
- 案例4:生成前端友好分页导航数据(页码列表、上一页/下一页)
- 常见问题与解答(Q&A)
- 分页工具封装的最佳实践
为什么需要封装分页工具?
在Web开发中,分页几乎无处不在:文章列表、商品展示、评论系统……
如果每次都要手写LIMIT offset, limit或skip().limit(),代码会变得冗余且难以维护。
封装分页工具的核心目标:

- 统一分页逻辑
- 自动处理边界情况(如页码越界、总数为0)
- 提供可复用的类或函数,适配不同数据源(列表、SQL、NoSQL)
许多团队在早期采用“分页参数靠手写”,但后期发现每改一个模型就要重写一遍分页代码——这就是封装的价值。
分页核心原理拆解
任何分页系统都围绕以下公式:
offset = (page - 1) * per_page
总页数 = ceil(total / per_page)
- page:当前页码(通常从1开始)
- per_page:每页记录数
- total:总记录数
- offset:SQL LIMIT的跳过量
边界处理:
- 当
page < 1时,自动修正为1 - 当
page > total_pages时,自动修正为最后一页 - 当
total == 0时,总页数为0
案例1:基础分页函数封装
def paginate(total, page, per_page=10):
total_pages = (total + per_page - 1) // per_page # 向上取整
page = max(1, min(page, total_pages)) if total > 0 else 1
offset = (page - 1) * per_page
return {
'page': page,
'per_page': per_page,
'total': total,
'total_pages': total_pages,
'offset': offset,
'has_prev': page > 1,
'has_next': page < total_pages
}
使用示例:
result = paginate(100, 2, per_page=10) print(result['offset']) # 输出 10
这个函数适合最简单的场景,但无法直接“附加”到数据查询上。
案例2:面向对象封装分页类
class Paginator:
def __init__(self, data, per_page=10):
self.data = data
self.per_page = per_page
self.total = len(data)
self.total_pages = (self.total + per_page - 1) // per_page if self.total > 0 else 1
def get_page(self, page):
page = max(1, min(page, self.total_pages)) if self.total > 0 else 1
start = (page - 1) * self.per_page
end = start + self.per_page
items = self.data[start:end] if self.total > 0 else []
return {
'items': items,
'page': page,
'per_page': self.per_page,
'total': self.total,
'total_pages': self.total_pages,
'has_prev': page > 1,
'has_next': page < self.total_pages
}
优势:
- 一次初始化,多次调取不同页码
- 支持直接传入数据列表(例如从API获取的列表)
案例3:对接数据库的分页封装
以MySQL + PyMySQL为例:
class DatabasePaginator:
def __init__(self, connection, table_name, per_page=10, where_clause='', order_by='id'):
self.conn = connection
self.table = table_name
self.per_page = per_page
self.where = where_clause
self.order_by = order_by
def get_page(self, page):
# 获取总数
total_sql = f"SELECT COUNT(*) FROM {self.table} {self.where}"
self.conn.execute(total_sql)
total = self.conn.fetchone()[0]
total_pages = (total + self.per_page - 1) // self.per_page if total > 0 else 1
page = max(1, min(page, total_pages)) if total > 0 else 1
offset = (page - 1) * self.per_page
data_sql = f"SELECT * FROM {self.table} {self.where} ORDER BY {self.order_by} LIMIT {offset}, {self.per_page}"
self.conn.execute(data_sql)
items = self.conn.fetchall()
return {
'items': items,
'page': page,
'per_page': self.per_page,
'total': total,
'total_pages': total_pages,
'has_prev': page > 1,
'has_next': page < total_pages
}
适配ORM(如SQLAlchemy):
class SQLAlchemyPaginator:
def __init__(self, query, per_page=10):
self.query = query
self.per_page = per_page
def get_page(self, page):
total = self.query.count()
total_pages = (total + self.per_page - 1) // self.per_page if total > 0 else 1
page = max(1, min(page, total_pages)) if total > 0 else 1
items = self.query.offset((page - 1) * self.per_page).limit(self.per_page).all()
return {...} # 同上
案例4:生成前端友好分页导航数据
很多前端框架(如Bootstrap)需要“页码列表”来渲染分页栏。
def generate_nav_data(page, total_pages, max_buttons=5):
"""
生成可供前端渲染的页码列表
max_buttons: 最多显示的页码按钮数量(不包括省略号)
"""
if total_pages == 0:
return []
# 计算起始和结束页码
half = max_buttons // 2
start = max(1, page - half)
end = min(total_pages, page + half)
# 调整以确保显示足够按钮
if end - start + 1 < max_buttons:
if start == 1:
end = min(total_pages, start + max_buttons - 1)
else:
start = max(1, end - max_buttons + 1)
pages = list(range(start, end + 1))
# 添加省略号标记
nav = []
if pages[0] > 1:
nav.append(1)
if pages[0] > 2:
nav.append('...')
nav.extend(pages)
if pages[-1] < total_pages:
if pages[-1] < total_pages - 1:
nav.append('...')
nav.append(total_pages)
return nav
# 使用示例
nav = generate_nav_data(5, 20) # 输出如 [1, '...', 3,4,5,6,7, '...', 20]
常见问题与解答(Q&A)
Q1:为什么不能用for循环直接分页列表?
A:如果数据集很大(例如数万条),一次性加载到内存会耗尽资源,数据库分页通过LIMIT只取当前页数据,更高效。
Q2:分页时页码越界了怎么办?
A:我们的封装全部做了边界修正:超上限自动设为最后一页,超下限设为第一页,这是用户友好设计。
Q3:如何支持不同数据库的分页?
A:通过适配器模式,例如SQLite不支持LIMIT offset的负值,但我们的分页逻辑保证offset≥0,对于MongoDB,改用skip().limit()。
Q4:封装好的分页工具如何单元测试?
A:可测试:
- 总数0时返回空列表
- 页码为负数自动修正
- 分页前后项总数正确
- 页码列表生成是否正确
分页工具封装的最佳实践
- 核心逻辑与数据源解耦:Paginator类只负责计算偏移量和边界,不关心数据具体来源。
- 统一返回结构:始终返回包含
items, page, total_pages, has_prev, has_next的字典。 - 注意性能:对于大数据集,避免在内存中计算总数,使用数据库的
COUNT(*)。 - 前端友好:提供生成页码导航列表的辅助函数,减少前端计算负担。
- 扩展性:允许传入自定义排序、过滤条件。
建议将分页工具作为独立模块放在utils/pagination.py中,团队内统一引用,这样后续升级分页逻辑(如增加缓存、支持游标分页)只需修改一处。
希望这个分页封装案例对你的项目有所帮助,如果还有具体场景(如异步分页、多种数据源混用)需要讨论,欢迎在评论区留言。