Python案例如何封装分页工具?

wen python案例 28

Python案例:如何优雅地封装一个分页工具?——从零构建可复用分页逻辑

📚 目录导读

  1. 为什么需要封装分页工具? —— 避免重复造轮子
  2. 分页核心原理拆解 —— 页码、偏移量与总数
  3. 案例1:基础分页函数封装 —— 掌握最小核心逻辑
  4. 案例2:面向对象封装分页类 —— 提升可扩展性
  5. 案例3:对接数据库(MySQL/ORM)的分页封装
  6. 案例4:生成前端友好分页导航数据(页码列表、上一页/下一页)
  7. 常见问题与解答(Q&A)
  8. 分页工具封装的最佳实践

为什么需要封装分页工具?

在Web开发中,分页几乎无处不在:文章列表、商品展示、评论系统……
如果每次都要手写LIMIT offset, limitskip().limit(),代码会变得冗余且难以维护。
封装分页工具的核心目标

Python案例如何封装分页工具?

  • 统一分页逻辑
  • 自动处理边界情况(如页码越界、总数为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时返回空列表
  • 页码为负数自动修正
  • 分页前后项总数正确
  • 页码列表生成是否正确

分页工具封装的最佳实践

  1. 核心逻辑与数据源解耦:Paginator类只负责计算偏移量和边界,不关心数据具体来源。
  2. 统一返回结构:始终返回包含items, page, total_pages, has_prev, has_next的字典。
  3. 注意性能:对于大数据集,避免在内存中计算总数,使用数据库的COUNT(*)
  4. 前端友好:提供生成页码导航列表的辅助函数,减少前端计算负担。
  5. 扩展性:允许传入自定义排序、过滤条件。

建议将分页工具作为独立模块放在utils/pagination.py中,团队内统一引用,这样后续升级分页逻辑(如增加缓存、支持游标分页)只需修改一处。


希望这个分页封装案例对你的项目有所帮助,如果还有具体场景(如异步分页、多种数据源混用)需要讨论,欢迎在评论区留言。

抱歉,评论功能暂时关闭!