Jupyter notebook (lab) 上で画像などが表示されるオブジェクトの作り方を勉強したのでメモ。


やり方

_repr_**_という関数を定義してあげることで実現できる。

現在対応している表示方法は以下。

Format REPL Notebook Qt Console
_repr_pretty_ yes yes yes
_repr_svg_ no yes yes
_repr_png_ no yes yes
_repr_jpeg_ no yes yes
_repr_html_ no yes no
_repr_javascript_ no yes no
_repr_markdown_ no yes no
_repr_latex_ no yes no
_repr_mimebundle_ o ? ?

IPython priority order of repr methods using display

上記の関数を定義することでJupyter上での出力を制御できる。

簡単な例を下記に示す。

モジュールのインポート

import io
import sys
from dataclasses import dataclass
from typing import Union

if sys.version_info >= (3, 10):
    from typing import TypeAlias
else:
    from typing_extensions import TypeAlias

from PIL.PngImagePlugin import PngImageFile

import IPython
from IPython.display import display_png, display_pretty

# display関数は v5.4およびv6.1以降は自動でインポートされるらしい。
# https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html#IPython.display.display
# from IPython.display import display

import py3Dmol
from rdkit import Chem
from rdkit.Chem import Draw

バージョン

print(sys.version)
3.11.6 | packaged by conda-forge | (main, Oct  3 2023, 10:37:07) [Clang 15.0.7 ]
print(IPython.__version__)
8.24.0
SMILES: TypeAlias = str


@dataclass  # 勝手にreprを綺麗にしてくれる
class Chemical:
    """化合物を表現するオブジェクト。SMILESとMolを入力にとることができる。

    Parameters
    ----------
    _chemical : Union[SMILES, Chem.Mol]
        `str` or `rdkit.Chem.rdchem.Mol`
    canonicalize : bool, optional
        canonicalize SMILES or not, by default False

    Raises
    ------
    TypeError
        文字列でもrdkit.Chem.rdchem.Molオブジェクトでもない場合にエラーを起こす。
    AttributeError
        molオブジェクト・smilesオブジェクトはimmutableになっているため、代入しようとするエラーを起こす。
    """

    _chemical: Union[SMILES, Chem.Mol]
    canonicalize: bool = False

    def __post_init__(self) -> None:
        if isinstance(self._chemical, str):
            self.__mol = Chem.MolFromSmiles(self._chemical)
            self.__smiles = (
                Chem.MolToSmiles(self.__mol)
                if self.canonicalize
                else self._chemical
            )
        elif isinstance(self._chemical, Chem.Mol):
            self.__mol = self._chemical
            self.__smiles = Chem.MolToSmiles(self._chemical)
        else:
            raise TypeError("`rdkit.Chem.rdchem.Mol` or `str`")

    @property
    def mol(self) -> Chem.Mol:
        """rdkit.Chem.rdchem.Mol"""
        return self.__mol

    @mol.setter
    def mol(self):
        raise AttributeError

    @property
    def smiles(self) -> SMILES:
        """SMILES (str)"""
        return self.__smiles

    @smiles.setter
    def smiles(self):
        raise AttributeError

    # 画像を表示するのに使う
    def _repr_png_(self) -> bytes:
        image_png: PngImageFile = Draw.MolToImage(self.mol)

        # bytes型に変換しなくてはならない
        # やり方参考: https://stackoverflow.com/questions/33101935/convert-pil-image-to-byte-array
        with io.BytesIO() as bytes_io:
            image_png.save(bytes_io, format="png")
            return bytes_io.getvalue()

    # HTMLを表示するのに使う
    def _repr_html_(self) -> str:
        # 参考: https://note.yu9824.com/howto/py3dmol-write-html/
        view = py3Dmol.view(width="100%")  # viewオブジェクトの生成
        view.addModel(
            Chem.MolToMolBlock(self.mol), "sdf", {"keepH": True}
        )  # viewオブジェクトにはMolBlock形式かPDB形式で分子を渡す必要があるため、`Chem.MolToMolBlock`関数でMolBlock形式に変換
        view.setStyle(
            {"stick": {"radius": 0.25}, "sphere": {"scale": 0.35}}
        )  # 分子の表示スタイルを設定
        return view._make_html()

上記のように定義してあげることで画像、テキスト、HTMLの3つの出力に対応したChemicalクラスが定義できた。

# ベンゼン
chemical = Chemical("c1ccccc1")
chemical

3Dmol.js failed to load for some reason. Please check your browser console for error messages.

勝手に表示されるこの動作は display 関数が非明示的に呼び出されていると考えれば良い。

# _repr_html_が優先して呼び出された
display(chemical)

3Dmol.js failed to load for some reason. Please check your browser console for error messages.

私の環境ではHTMLが優先されるようだ。

reprの優先度は変更できる?

→ できない。

一般に、オブジェクトが表示されるときには利用可能なフォーマッタがすべて呼び出されます。どのフォーマッタを表示するかは UI が決めることです。あるフォーマッタは、他のどのフォーマッタが使えるかによって出力を変えるべきではありません。

Integrating your objects with IPython より筆者が翻訳

つまり、どのreprが呼ばれるかはフロント側が決めるべき、という思想らしい。

jupyterにおける優先順位

特定のreprを呼び出したい場合は?

他のreprを呼び出したい場合は、明示的にそれ専用のdisplay関数を使用するのが一番簡単そう。

# _repr_png_を明示的に呼び出す
display_png(chemical)

2024-05-25-jupyter-display-instance_21_0.png

display_pretty(chemical)
Chemical(_chemical='c1ccccc1', canonicalize=False)

もしくは、display関数を使って、表示したいフォーマットや表示したくないフォーマットを指定すれば良い。

display(chemical, exclude=("text/html", "image/png"))
Chemical(_chemical='c1ccccc1', canonicalize=False)
display(chemical, include=("text/plain",))
Chemical(_chemical='c1ccccc1', canonicalize=False)

include, excludeに指定できる一覧 (っぽいもの)

  • text/plain
  • text/html
  • text/markdown
  • text/latex
  • application/json
  • application/javascript
  • application/pdf
  • image/png
  • image/jpeg
  • image/svg+xml

Module: core.formatters

ちなみにどの _repr_*_も定義されていない場合は組み込みの __repr__が呼ばれるらしい。

class NoRepr:
    def __init__(self) -> None: ...

    def __repr__(self) -> None:
        return "call `__repr__`"


NoRepr()
call `__repr__`

参考