DjangoのPaginatorクラスを理解する

## 初めに
Djangoでリスト表示する場合、```Paginator```クラスを使用することで簡単に実装することができます。
しかし、特定の要件やビジネスロジックを満たすためにページネーションの挙動を変更しなければいけない場合があります。
そんな時に```Paginator```クラスを理解していれば、容易にカスタマイズすることができると考えまとめることにしました。

全体コードは[こちら](https://github.com/django/django/blob/3.1/django/core/paginator.py)から

## Paginatorクラス
オブジェクトのリストをページ単位で分割する機能を提供します。
これにより、大量のデータを扱う際のユーザーインターフェイスが向上します。
```python:Paginatorクラス
class Paginator:

    def __init__(self, object_list, per_page, orphans=0,
                 allow_empty_first_page=True):
        self.object_list = object_list
        self._check_object_list_is_ordered()
        self.per_page = int(per_page)
        self.orphans = int(orphans)
        self.allow_empty_first_page = allow_empty_first_page

    def __iter__(self):
        for page_number in self.page_range:
            yield self.page(page_number)

    def validate_number(self, number):
        try:
            if isinstance(number, float) and not number.is_integer():
                raise ValueError
            number = int(number)
        except (TypeError, ValueError):
            raise PageNotAnInteger(_('That page number is not an integer'))
        if number < 1:
            raise EmptyPage(_('That page number is less than 1'))
        if number > self.num_pages:
            if number == 1 and self.allow_empty_first_page:
                pass
            else:
                raise EmptyPage(_('That page contains no results'))
        return number

    def get_page(self, number):
        try:
            number = self.validate_number(number)
        except PageNotAnInteger:
            number = 1
        except EmptyPage:
            number = self.num_pages
        return self.page(number)

    def page(self, number):
        number = self.validate_number(number)
        bottom = (number - 1) * self.per_page
        top = bottom + self.per_page
        if top + self.orphans >= self.count:
            top = self.count
        return self._get_page(self.object_list[bottom:top], number, self)

    def _get_page(self, *args, **kwargs):
        return Page(*args, **kwargs)

    @cached_property
    def count(self):
        c = getattr(self.object_list, 'count', None)
        if callable(c) and not inspect.isbuiltin(c) and method_has_no_args(c):
            return c()
        return len(self.object_list)

    @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)
        return ceil(hits / self.per_page)

    @property
    def page_range(self):
        return range(1, self.num_pages + 1)

    def _check_object_list_is_ordered(self):
        ordered = getattr(self.object_list, 'ordered', None)
        if ordered is not None and not ordered:
            obj_list_repr = (
                '{} {}'.format(self.object_list.model, self.object_list.__class__.__name__)
                if hasattr(self.object_list, 'model')
                else '{!r}'.format(self.object_list)
            )
            warnings.warn(
                'Pagination may yield inconsistent results with an unordered '
                'object_list: {}.'.format(obj_list_repr),
                UnorderedObjectListWarning,
                stacklevel=3
            )
```

### 初期化
Paginatorは以下の引数で初期化されます。
| 変数名 | 説明 |
| ---- | ---- |
| object_list | ページネーションが適用されるオブジェクトのリストで、データベースから取得したレコードのリストや、任意のPythonのリスト等が該当します。 |
| per_page | 1ページあたりに表示するオブジェクトの最大数を示します |
| orphans | この変数の値より少ないアイテムが最後のページにある場合、それ等を前のページに組み込むことができます |
| allow_empty_first_page | 最初のページが空であることを許可するかどうかをbool値で指定します。False に設定すると、空のリストでページネーションを試みるときに例外が発生します。 |

### オブジェクトリストのチェック
初期化の一環として、`_check_object_list_is_ordered`メソッドが呼び出され、`object_list`が順序付けられているかどうかを確認します。

### ページネーションの計算
- `count`: オブジェクトの総数を返します。
- `num_pages`: ページの総数を返します。
- `page_range`: ページ番号の範囲を返します。

### ページの取得と検証
- `get_page`: 指定されたページ番号に対して検証を行い、有効な`Page`オブジェクトを返します。

### イテレーション
`__iter__`メソッドを使用すると、`Paginator`インスタンスをイテレートすることができます。これにより、`self.page_range`に基づいて順番にページが生成されます。

### ページオブジェクトの生成
`page`メソッドは、指定されたページ番号に基づいて`Page`オブジェクトを生成します。

### ページのナビゲーション
`Page`クラスには、次のページや前のページへのナビゲーションを可能にするメソッドが用意されています。

このメソッドにより、ページネーション対象のオブジェクトを、```per_page```で指定した1ページあたりに表示する数に分割して表示することができます。

## __iter__メソッド
このメソッドにより、Paginatorインスタンスをイテレートして、各ページを順に取得できます。
```python:__iter__メソッド
def __iter__(self):
        for page_number in self.page_range:
            yield self.page(page_number)
```

```python:例
items = ['アイテム1', 'アイテム2', 'アイテム3', 'アイテム4', 'アイテム5', 'アイテム6', 'アイテム7', 'アイテム8', 'アイテム9', 'アイテム10', 'アイテム11', 'アイテム12']
paginator = Paginator(items, 3)

for page in paginator:
    print(f"ページ {page.number}:")
    for item in page:
        print(item)
    print("\n---\n")
```

```yaml:結果
ページ 1:
アイテム1
アイテム2
アイテム3

---

ページ 2:
アイテム4
アイテム5
アイテム6

---

.....
```
```page_range```メソッドでは1から最大ページ数までの範囲を取得しており、この値を元にループを行います。
page_rangeメソッドでは```num_pages```、```count```メソッドが使用されています。

そしてこのループ内で```page```メソッドを呼び出して、その番号に対応するページを取得します。

## pageメソッド
pageメソッドは、指定されたページ番号に基づいてPageオブジェクトを作成し、そのページに含まれるオブジェクトのサブセットを返します。

```python:pageメソッド
def page(self, number):
        """Return a Page object for the given 1-based page number."""
        number = self.validate_number(number)
        bottom = (number - 1) * self.per_page
        top = bottom + self.per_page
        if top + self.orphans >= self.count:
            top = self.count
        return self._get_page(self.object_list[bottom:top], number, self)
```
引数のページ番号をもとにそのページのアイテムが開始されるインデックス（```bottom```）と終了されるインデックス（```top```）を計算して```_get_page```メソッドを呼び出すことで(厳密には```Page```クラスを使用して)、その範囲にあるアイテムのスライスとページ番号、そしてPaginator自身を引数としてPageオブジェクトを生成します。

## Pageクラス
Pageクラスは、特定のページに関連するオブジェクトの集合を管理します。これには、ページのナビゲーションや、ページ内のアイテムへのアクセスなどが含まれます。

```python:Pageクラス
class Page(collections.abc.Sequence):

    def __init__(self, object_list, number, paginator):
        self.object_list = object_list
        self.number = number
        self.paginator = paginator

    def __repr__(self):
        return '<Page %s of %s>' % (self.number, self.paginator.num_pages)

    def __len__(self):
        return len(self.object_list)

    def __getitem__(self, index):
        if not isinstance(index, (int, slice)):
            raise TypeError(
                'Page indices must be integers or slices, not %s.'
                % type(index).__name__
            )
        # The object_list is converted to a list so that if it was a QuerySet
        # it won't be a database hit per __getitem__.
        if not isinstance(self.object_list, list):
            self.object_list = list(self.object_list)
        return self.object_list[index]

    def has_next(self):
        return self.number < self.paginator.num_pages

    def has_previous(self):
        return self.number > 1

    def has_other_pages(self):
        return self.has_previous() or self.has_next()

    def next_page_number(self):
        return self.paginator.validate_number(self.number + 1)

    def previous_page_number(self):
        return self.paginator.validate_number(self.number - 1)

    def start_index(self):
        # Special case, return zero if no items.
        if self.paginator.count == 0:
            return 0
        return (self.paginator.per_page * (self.number - 1)) + 1

    def end_index(self):
        # Special case for the last page because there can be orphans.
        if self.number == self.paginator.num_pages:
            return self.paginator.count
        return self.number * self.paginator.per_page
```

### 属性
| 変数名 | 説明 |
| ---- | ---- |
| object_list | ページに含まれるオブジェクトのリスト |
| number | ページ番号 |
| object_list | 親Paginatorオブジェクトの参照 |

### ヘルパーメソッド
| メソッド名 | 説明 |
| ---- | ---- |
| has_next | 次のページが存在するかどうかを返します |
| has_previous | 前のページが存在するかどうかを返します |
| has_other_pages | 他のページが存在するかどうかを返します |
| next_page_number | 次のページの番号を返します |
| previous_page_number | 前のページの番号を返します |
| start_index | このページに含まれるオブジェクトの開始インデックス |
| end_index | このページに含まれるオブジェクトの終了インデックス |

これらの機能により、Pageオブジェクトを使用することで、特定のページのコンテキストでページネーションに関連する情報を簡単に取得することができます

## カスタム例
以下では、PaginatorクラスとPageクラスを継承してオーバーライドしています。
```python
from django.core.paginator import Paginator, Page

class CustomPage(Page):
    def get_highlighted_items(self):
        return ["**{}**".format(item) if len(item) >= 5 else item for item in self.object_list]

class CustomPaginator(Paginator):
    def _get_page(self, *args, **kwargs):
        return CustomPage(*args, **kwargs)

# 使用例
objects = ['apple', 'banana', 'cherry', 'date', 'fig', 'grape']
custom_paginator = CustomPaginator(objects, 2)
page1 = custom_paginator.page(1)

# カスタムメソッドを使用
highlighted_items = page1.get_highlighted_items()
print(highlighted_items)  # ['apple', '**banana**']
```
CustomPageクラスでは、```Page```クラスを継承し、条件に基づいてアイテムにハイライトを追加する新しいメソッド```get_highlighted_items```を提供します。

```CustomPaginator```クラスでは```_get_page```メソッドをオーバーライドして、カスタムページオブジェクト CustomPage を生成して返しています。

```custom_paginator.page(1)```で```page```メソッドを使用して、ページ番号1に相当するページを取得しています。
後は追加した```get_highlighted_items```メソッドを使用することでページ1の文字列長が5以上のアイテムに対してハイライトされるようになります。