Django 分页器 Paginator

概述

Django的分页器系统是一个精心设计的组件,用于处理大量数据的分页显示。它由两个核心类组成:

  • Paginator:分页器主类,负责分页逻辑和元数据计算
  • Page:单个页面类,表示具体某一页的数据和导航信息

核心设计原理

1. 分页器初始化

1
2
3
4
5
6
class Paginator(object):
def __init__(self, object_list, per_page, orphans=0, allow_empty_first_page=True):
self.object_list = object_list # 要分页的数据列表
self.per_page = int(per_page) # 每页显示多少条数据
self.orphans = int(orphans) # 孤立条目数(避免最后一页只有很少数据)
self.allow_empty_first_page = allow_empty_first_page # 是否允许第一页为空

orphans参数的巧妙设计

  • 如果最后一页的项目数少于或等于orphans,这些项目会被添加到前一页
  • 避免出现最后一页只有1-2条数据的尴尬情况

2. 核心分页算法

总页数计算算法

1
2
3
4
5
6
7
8
@cached_property
def num_pages(self):
if self.count == 0 and not self.allow_empty_first_page:
return 0
# 总记录数,减去孤立的条目数,确保至少有一页
hits = max(1, self.count - self.orphans)
# 计算总页数,使用ceil函数向上取整
return int(ceil(hits / float(self.per_page)))

算法精髓

  • hits = max(1, self.count - self.orphans) 确保至少有1页
  • 通过ceil向上取整,保证所有数据都能被包含
  • orphans参数会减少总的有效记录数,从而可能减少页数

页面数据获取算法

1
2
3
4
5
6
7
8
9
10
def page(self, number):
number = self.validate_number(number)
bottom = (number - 1) * self.per_page # 起始索引
top = bottom + self.per_page # 结束索引

# 特殊处理最后一页:包含orphan数据
if top + self.orphans >= self.count:
top = self.count

return self._get_page(self.object_list[bottom:top], number, self)

关键逻辑

  • 使用切片操作 object_list[bottom:top] 获取页面数据
  • 最后一页特殊处理:将剩余的orphan数据包含进来

数据总数获取策略

1
2
3
4
5
6
7
8
@cached_property
def count(self):
try:
# 优先使用count()方法(适用于QuerySet)
return self.object_list.count()
except (AttributeError, TypeError):
# 降级使用len()函数(适用于list等)
return len(self.object_list)

兼容性设计

  • QuerySet.count() → 执行 SELECT COUNT(*) SQL查询
  • list/tuple → 使用Python内置的 len() 函数

@cached_property装饰器深度解析

实现原理

1
2
3
4
5
6
7
8
9
10
11
12
class cached_property(object):
def __init__(self, func, name=None):
self.func = func
self.__doc__ = getattr(func, '__doc__')
self.name = name or func.__name__

def __get__(self, instance, cls=None):
if instance is None:
return self
# 核心:一次计算,存储到实例字典,后续直接读取
res = instance.__dict__[self.name] = self.func(instance)
return res

工作机制

  1. 描述符协议:实现了__get__方法,控制属性访问
  2. 缓存策略:将计算结果存储在instance.__dict__
  3. 访问优化:利用Python属性查找顺序,后续访问直接从实例字典获取

链式赋值的巧妙运用

1
res = instance.__dict__[self.name] = self.func(instance)

这行代码体现了Python链式赋值的优雅:

  • 只调用一次函数self.func(instance)只执行一次
  • 同时完成缓存和返回:一步到位
  • 代码简洁:避免临时变量

Page类的设计亮点

1. 实现Sequence协议

1
2
3
4
5
6
7
8
9
class Page(collections.Sequence):
def __len__(self):
return len(self.object_list)

def __getitem__(self, index):
# 支持索引和切片访问
if not isinstance(self.object_list, list):
self.object_list = list(self.object_list) # QuerySet优化
return self.object_list[index]

2. 导航方法

1
2
3
4
5
6
7
8
9
def start_index(self):
# 返回当前页第一个对象在整个列表中的位置(1基索引)
return (self.paginator.per_page * (self.number - 1)) + 1

def end_index(self):
# 特殊处理最后一页的orphan数据
if self.number == self.paginator.num_pages:
return self.paginator.count
return self.number * self.paginator.per_page

性能优化策略

1. 缓存层次

1
2
3
4
5
6
7
# 第一层:@cached_property缓存
paginator.count # 第1次:SQL查询 + 缓存
paginator.count # 第2次:直接使用缓存
paginator.num_pages # 使用count缓存,无额外SQL

# 第二层:QuerySet级别优化
page.object_list # QuerySet → list转换,避免重复数据库访问

2. QuerySet优化

1
2
3
4
5
6
# Page类中的优化
def __getitem__(self, index):
# 将QuerySet转为list,避免每次__getitem__都查询数据库
if not isinstance(self.object_list, list):
self.object_list = list(self.object_list)
return self.object_list[index]

3. 数据库查询优化

1
2
3
4
5
# QuerySet.count()实现
def count(self):
if self._result_cache is not None:
return len(self._result_cache) # 如果已缓存,直接返回
return self.query.get_count(using=self.db) # 执行SELECT COUNT(*)

性能与数据一致性的权衡

核心问题

1
2
3
4
5
6
7
8
9
# 数据一致性问题演示
paginator = Paginator(Article.objects.all(), 10)
print(paginator.count) # 100条记录

# 在别处删除了数据
Article.objects.filter(id__lte=10).delete()

# 分页器缓存未更新!
print(paginator.count) # 仍然显示100条(不一致!)

Django的解决策略:请求级缓存

1. 短生命周期设计

1
2
3
4
5
6
7
8
9
10
11
def article_list(request):
# 每个HTTP请求创建新的分页器实例
paginator = Paginator(Article.objects.all(), 10)

# 在请求期间,缓存有效
count1 = paginator.count # SQL查询 + 缓存
count2 = paginator.count # 使用缓存
num_pages = paginator.num_pages # 使用count缓存

return render(request, 'list.html', {'paginator': paginator})
# 请求结束,实例销毁,缓存消失

2. 实例级隔离

1
2
3
4
5
# 不同请求,不同实例,独立缓存
request_1_paginator = Paginator(queryset, 10) # 实例1
request_2_paginator = Paginator(queryset, 10) # 实例2(独立)

# 两者缓存完全隔离,互不影响

权衡分析

方案 性能 一致性 复杂度 适用场景
无缓存 强一致 数据变化频繁
请求级缓存 请求内一致 Web应用(Django选择)
全局缓存 最好 弱一致 读多写少
TTL缓存 较好 最终一致 可接受延迟

Django选择的优势

  1. 简单可靠:不需要复杂的缓存失效策略
  2. 内存安全:随请求结束自动清理,无内存泄漏
  3. 并发安全:每个请求独立,无竞态条件
  4. 适合Web场景:符合HTTP请求的无状态特性

实际应用场景

1. 基础分页

1
2
3
4
5
6
7
8
9
10
11
def article_list(request):
articles = Article.objects.all().order_by('-created_at')
paginator = Paginator(articles, 10)

page_number = request.GET.get('page', 1)
page = paginator.page(page_number)

return render(request, 'list.html', {
'page': page,
'paginator': paginator
})

2. 模板中的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!-- 分页信息 -->
<div class="pagination-info">
共 {{ paginator.count }} 条记录,分为 {{ paginator.num_pages }} 页
当前第 {{ page.number }} 页
</div>

<!-- 页面导航 -->
<div class="pagination">
{% if page.has_previous %}
<a href="?page={{ page.previous_page_number }}">上一页</a>
{% endif %}

{% for num in paginator.page_range %}
{% if num == page.number %}
<strong>{{ num }}</strong>
{% else %}
<a href="?page={{ num }}">{{ num }}</a>
{% endif %}
{% endfor %}

{% if page.has_next %}
<a href="?page={{ page.next_page_number }}">下一页</a>
{% endif %}
</div>

3. orphans参数的应用

1
2
3
4
5
6
# 避免最后一页只有很少数据
paginator = Paginator(articles, per_page=10, orphans=3)

# 示例:103条记录
# 不使用orphans: 10页 (10+10+...+10+3)
# 使用orphans=3: 9页 (10+10+...+10+13)

高级扩展

1. 自定义分页器

1
2
3
4
5
6
7
8
9
10
11
class SmartPaginator(Paginator):
def refresh_count(self):
"""手动刷新计数缓存"""
if 'count' in self.__dict__:
del self.__dict__['count']
if 'num_pages' in self.__dict__:
del self.__dict__['num_pages']

def get_fresh_count(self):
"""获取最新计数,绕过缓存"""
return self.object_list.count()

2. TTL缓存实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class TTLPaginator(Paginator):
def __init__(self, *args, cache_ttl=60, **kwargs):
super().__init__(*args, **kwargs)
self.cache_ttl = cache_ttl
self._count_cache_time = 0
self._count_cache_value = None

@property
def count(self):
import time
current_time = time.time()

if (self._count_cache_value is None or
current_time - self._count_cache_time > self.cache_ttl):
self._count_cache_value = self.object_list.count()
self._count_cache_time = current_time

return self._count_cache_value

最佳实践

1. 性能优化

1
2
3
4
5
6
7
# ✅ 正确:添加排序,避免警告
articles = Article.objects.all().order_by('-id')
paginator = Paginator(articles, 10)

# ❌ 错误:无序QuerySet可能导致分页结果不一致
articles = Article.objects.all() # 无order_by
paginator = Paginator(articles, 10) # 会产生警告

2. 数据库查询优化

1
2
3
4
5
6
7
# ✅ 使用select_related减少数据库查询
articles = Article.objects.select_related('author').order_by('-created_at')
paginator = Paginator(articles, 10)

# ✅ 使用prefetch_related优化反向关系
articles = Article.objects.prefetch_related('tags').order_by('-created_at')
paginator = Paginator(articles, 10)

3. 错误处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from django.core.paginator import EmptyPage, PageNotAnInteger

def article_list(request):
paginator = Paginator(articles, 10)
page_number = request.GET.get('page', 1)

try:
page = paginator.page(page_number)
except PageNotAnInteger:
page = paginator.page(1)
except EmptyPage:
page = paginator.page(paginator.num_pages)

return render(request, 'list.html', {'page': page})

总结

Django分页器的设计体现了以下几个核心原则:

  1. 实用主义:选择适合Web应用场景的缓存策略
  2. 性能优化:通过@cached_property避免重复计算
  3. 简单可靠:请求级缓存,避免复杂的失效机制
  4. 向后兼容:支持QuerySet和普通列表
  5. 用户友好:orphans参数优化用户体验

这个设计在性能、一致性和复杂度之间找到了最佳平衡点,是Web框架设计的典型范例。


Django 分页器 Paginator
https://luffy997.github.io/2025/07/02/Django分页器-Paginator/
作者
Luffy997
发布于
2025年7月2日
许可协议