IPython架构之Display

作者 : RY    标签: ipython-notebook

本文分析IPython是如何将对象转换为显示信息的,如何自定义对象的显示。

IPython显示对象的原理

用户代码在Kernel中的shell对象中执行之后,通过DisplayFormatter将需要显示的对象转换为一个描述显示信息的字典,并传递给客户端进行显示。目前IPython有三种客户端:控制台、QtConsole和Notebook。每种客户端都会从这个字典中提取自己能处理的信息进行显示。下面首先获得shell对象,它是InteractiveShell类的唯一实例:

from IPython.core.interactiveshell import InteractiveShell
sh = InteractiveShell.instance()

shell中的display_formatter是用来将对象转换为显示信息的DisplayFormatter对象:

sh.display_formatter
<IPython.core.formatters.DisplayFormatter at 0x9ed190c>

DisplayFormatter对象的formatters字典中保存所有用于显示转换的对象:

sh.display_formatter.formatters
{u'application/javascript': <IPython.core.formatters.JavascriptFormatter at 0x9f704cc>,
 u'application/json': <IPython.core.formatters.JSONFormatter at 0x9f7064c>,
 u'image/jpeg': <IPython.core.formatters.JPEGFormatter at 0x9f7062c>,
 u'image/png': <IPython.core.formatters.PNGFormatter at 0x9f7060c>,
 u'image/svg+xml': <IPython.core.formatters.SVGFormatter at 0x9f70a6c>,
 u'text/html': <IPython.core.formatters.HTMLFormatter at 0x9f70a2c>,
 u'text/latex': <IPython.core.formatters.LatexFormatter at 0x9f7418c>,
 u'text/plain': <IPython.core.formatters.PlainTextFormatter at 0x9f70eac>}

DisplayFormatter对象的format()方法会遍历这个字典,尝试用每个Formatter对象进行转换,如果转换成功则保存到结果中。下面将”hello world”转换为输出字典,由于”hello world”只能被当作文本输出,因此结果字典中只有u’text/plain’一个键值。

disformat = sh.display_formatter.format
disformat("hello world")
{u'text/plain': "'hello world'"}

display模块中有许多将对象包装为某种特殊显示的类,例如display.Javascript会将字符串当作javascript输出。下面的例子中,输出字典中有u’text/plain’和u’application/javascript’两个键,客户端根据自己的显示能力决定使用何种格式显示。由于Notebook客户端可以执行Javascript程序,因此它会选择u’application/javascript’。

from IPython import display
disformat(display.Javascript('alert("hello world")'))
{u'application/javascript': 'alert("hello world")',
 u'text/plain': '<IPython.core.display.Javascript at 0x9f7092c>'}

上面我们通过DisplayFormatter对象的format()方法显示了对象进行显示转换之后的字典。如果我们直接让shell执行程序得到一个Javascript对象,那么这段Javascript代码就会在浏览器中运行。在Notebook中执行下面程序将会看到一个显示”hello world”的对话框。

display.Javascript('alert("hello world")')
<IPython.core.display.Javascript at 0x9f7068c>

下面我们看看如何用display.Image显示图像。当通过url关键字参数指定图像的URL时,实际上会使用一段HTML显示图像:

logourl = "http://www.python.org/community/logos/python-powered-h-50x65.png"
disformat(display.Image(url=logourl))
{u'text/html': u'<img src="http://www.python.org/community/logos/python-powered-h-50x65.png"/>',
 u'text/plain': '<IPython.core.display.Image at 0x9f700ec>'}

但是如果指定embed参数为True,那么shell会将图片下载下来,将图片内容发送给客户端:

print str(disformat(display.Image(url=logourl, embed=True)))[:100]
{u’image/png’: ‘x89PNGrnx1anx00x00x00rIHDRx00x00x002x00x00x00Ax08x06x00x00x00x9

因此我们也可以图像处理扩展库在内存中创建一副图像,并通过Image包装传递给客户端。下面的程序先创建一个表示图像的数组img,然后调用OpenCV模块cv2的blur()对图像进行模糊处理,并调用imencode()将数组压缩为PNG格式的图像,imencode()的输出是表示图像内容的字符串,我们可以通过Image类将字符串以u’image/png’格式发送给客户端进行显示。

import cv2
import numpy as np
img = np.random.randint(0,255,(300,300,3))
cv2.blur(img, (11,11), img)
r, dat = cv2.imencode(".png", img)
display.Image(dat.tostring())
random01.png

Formatter对象

每种输出格式都对应一个Formatter对象,它们被保存在DisplayFormatter对象的formatters字典中,下面我们通过其中的TextFormatter,看看它们是如何工作的:

text_formatter = sh.display_formatter.formatters[u'text/plain']

每个Formatter对象在进行转换的时候,都经过如下四个步骤:

  • 如果被转换对象的id为singleton_printers字典中的键,则使用id对应的转换函数进行转换。
  • 如果被转换对象的类型为type_printers字典中的键,则使用类型对应的转换函数进行转换。
  • 如果通过被转换对象的类型计算的元组(模块名,类型名)为deferred_printers字典中的键,则使用其对应的转换函数进行转换。
  • 如果被转换对象有某种特殊的方法名,则调用此方法进行转换。

下面我们看看text_formatter的三个转换字典:

text_formatter.singleton_printers
{136823052: <function IPython.lib.pretty._repr_pprint>,
 136823060: <function IPython.lib.pretty._repr_pprint>,
 136851940: <function IPython.lib.pretty._repr_pprint>,
 136851952: <function IPython.lib.pretty._repr_pprint>,
 136851968: <function IPython.lib.pretty._repr_pprint>}
text_formatter.type_printers
{_sre.SRE_Pattern: <function IPython.lib.pretty._re_pattern_pprint>,
 instancemethod: <function IPython.lib.pretty._repr_pprint>,
 dictproxy: <function IPython.lib.pretty.inner>,
 xrange: <function IPython.lib.pretty._repr_pprint>,
 set: <function IPython.lib.pretty.inner>,
 frozenset: <function IPython.lib.pretty.inner>,
 slice: <function IPython.lib.pretty._repr_pprint>,
 super: <function IPython.lib.pretty._super_pprint>,
 BaseException: <function IPython.lib.pretty._exception_pprint>,
 builtin_function_or_method: <function IPython.lib.pretty._function_pprint>,
 function: <function IPython.lib.pretty._function_pprint>,
 classobj: <function IPython.lib.pretty._type_pprint>,
 dict: <function IPython.lib.pretty.inner>,
 list: <function IPython.lib.pretty.inner>,
 type: <function IPython.lib.pretty._type_pprint>,
 unicode: <function IPython.lib.pretty._repr_pprint>,
 str: <function IPython.lib.pretty._repr_pprint>,
 tuple: <function IPython.lib.pretty.inner>,
 float: <function IPython.core.formatters.<lambda>>,
 long: <function IPython.lib.pretty._repr_pprint>,
 int: <function IPython.lib.pretty._repr_pprint>,
 datetime.timedelta: <function IPython.lib.pretty._repr_pprint>,
 datetime.datetime: <function IPython.lib.pretty._repr_pprint>}
text_formatter.deferred_printers
{}

singleton_printers字典的键是对象的id,为了知道具体的对象,我们需要通过下面的程序将id转换为对象:

import ctypes
for key in text_formatter.singleton_printers:
    print ctypes.cast(ctypes.c_void_p(key), ctypes.py_object).value
None
False
True
Ellipsis
NotImplemented

在所有的Formatter对象中,只有text_formatter知道如何转换False对象,因此下面程序的输出字典中只有通过text_formatter得到的结果。

disformat(False)
{u'text/plain': 'False'}

如果我们希望用某种特殊的样式在Notebook中显示False对象,可以修改HTMLFormatter对象的singleton_printers字典。下面的程序先获得HTMLFormatter对象,然后在其singleton_printers字典中添加显示False的函数my_formatter:

html_formatter = sh.display_formatter.formatters[u'text/html']
def my_formatter(obj):
    return '<span style="color:red;font-size:18px">too bad, it\'s %s</span>' % str(obj)
html_formatter.singleton_printers[id(False)] = my_formatter
1 == 2
False

LatexFormatter采用LaTeX显示对象,可以用于显示数学公式,例如下面的程序在LatexFormatter的type_printers字典中添加对Fraction类的转换函数:

from fractions import Fraction
latex_formatter = sh.display_formatter.formatters[u"text/latex"]
def fraction_formatter(obj):
    return '$$\frac{%d}{%d}$$' % (obj.numerator, obj.denominator)
latex_formatter.type_printers[Fraction] = fraction_formatter
Fraction(3, 4) ** 4 / 3

\frac{27}{256}

Fraction(27, 256)

deferred_printers字典和type_printers字典类似,不过它采用包含模块名和类名的元组作为键,这样可以避免在定义转换函数时载入对应的类。可以对用户的Python环境中可能不存在的类提供转换函数。

下面的程序在PNGFormatter中的deferred_printers字典中添加将特定属性的数组转换为PNG图像的函数,当转换函数返回None时,表示忽略其转换结果。

png_formatter = sh.display_formatter.formatters[u'image/png']
def ndarray_formatter(array):
    if array.dtype == np.uint8 and array.ndim == 3 and array.shape[-1] == 3:
        import cv2
        r, dat = cv2.imencode(".png", array)
        return dat.tostring()
    else:
        return None
png_formatter.deferred_printers[("numpy", "ndarray")] = ndarray_formatter
np.random.randint(0,255,(300,300,3)).astype(np.uint8)
random02.png

对于不满足特定条件的数组,仍然使用数组的u’text/plain’转换结果进行显示:

np.arange(10)
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

最后,Formatter将调用特定的方法进行显示转换,例如HTMLFormatter对应的方法名为_repr_html_。因此下面的Color类可以被HTMLFormatter转换为HTML显示:

class Color(object):
    def __init__(self, r, g, b):
        self.rgb = (r, g, b)

    def html_color(self):
        return "#%x%x%x" % self.rgb

    def _repr_html_(self):
        c = self.html_color()
        return '<span style="color:%s">%s</span>' % (c, c)

Color(255, 120, 150)
<__main__.Color at 0xa13a4ec>

相关代码

在Notebook中输入如下命令,可以查看相关对象的代码。

循环调用各个Formatter,并将结果收集到一个字典中

IPython.core.formatters.DisplayFormatter??

所有Formatter的基类

IPython.core.formatters.BaseFormatter??

将对象显示为文本Formatter

IPython.core.formatters.PlainTextFormatter??

loading...