Pythonのインタプリタを簡単に拡張して使いやすくする

# Pythonのインタプリタを少しの手間で簡単に拡張して使いやすくする

Pythonには簡単にモジュールの使い方などを確認できる**インタプリタ**がありますが、これを少しの手間で簡単に拡張して使いやすくすることができます。今回はPythonのインタプリタで**Pygments**を使ってコードをハイライトさせてみましょう。

注：この記事では **Python2.7.5** を使っています。

## 必要なライブラリ

インタプリタを拡張するにあたって、必要なライブラリが二つあります。

* **prompt_toolkit** - https://github.com/jonathanslenders/python-prompt-toolkit
* **Pygments** - http://pygments.org/

これらをpipでインストールしてください。

```
$ pip install prompt_toolkit
$ pip install Pygments
```

## どうやって拡張するか

Pythonのインタプリタを拡張するためには、Pythonに標準で搭載されている **code.py** というモジュールを使います。
このモジュールについては、

* [Pythonのインタプリタをスクリプト内で実行させる](https://qiita.com/Alice1017/items/74ec054f357f4f292cd9)
* [29.1. code — インタプリタ基底クラス(python2.7)](https://docs.python.jp/2.7/library/code.html)
* [30.1. code — インタプリタ基底クラス(python3.5)](https://docs.python.jp/3.5/library/code.html)

これらの記事、ドキュメントを参照してください。まぁ、簡単に言うとcode.pyは**PythonでPythonのインタプリタを実装した**ものです。このcode.pyの**InteractiveConsole**クラスを**継承**することによって、インタプリタを拡張していきます。

code.pyのインタプリタ実行部分のコードはこんな感じです

```py
def interact(self, banner=None):
        """Closely emulate the interactive Python console.

        The optional banner argument specify the banner to print
        before the first interaction; by default it prints a banner
        similar to the one printed by the real Python interpreter,
        followed by the current class name in parentheses (so as not
        to confuse this with the real interpreter -- since it's so
        close!).

        """
        try:
            sys.ps1
        except AttributeError:
            sys.ps1 = ">>> "
        try:
            sys.ps2
        except AttributeError:
            sys.ps2 = "... "
        cprt = 'Type "help", "copyright", "credits" or "license" for more information.'
        if banner is None:
            self.write("Python %s on %s\n%s\n(%s)\n" %
                       (sys.version, sys.platform, cprt,
                        self.__class__.__name__))
        else:
            self.write("%s\n" % str(banner))
        more = 0
        while 1:
            try:
                if more:
                    prompt = sys.ps2
                else:
                    prompt = sys.ps1
                try:
                    line = self.raw_input(prompt)
                    # Can be None if sys.stdin was redefined
                    encoding = getattr(sys.stdin, "encoding", None)
                    if encoding and not isinstance(line, unicode):
                        line = line.decode(encoding)
                except EOFError:
                    self.write("\n")
                    break
                else:
                    more = self.push(line)
            except KeyboardInterrupt:
                self.write("\nKeyboardInterrupt\n")
                self.resetbuffer()
                more = 0
```

今回は、`line = self.raw_input(prompt)`この部分を書き換えることによって、インタプリタを拡張していきます。

## 手順①　InteractiveConsoleクラスを承継したクラスを作る

```py
from code import InteractiveConsole

class EnhancedPythonConsole(InteractiveConsole):

    # とりあえずコピー＆ペースト
    def interact(self, banner=None):
        """Closely emulate the interactive Python console.

        The optional banner argument specify the banner to print
        before the first interaction; by default it prints a banner
        similar to the one printed by the real Python interpreter,
        followed by the current class name in parentheses (so as not
        to confuse this with the real interpreter -- since it's so
        close!).

        """
        try:
            sys.ps1
        except AttributeError:
            sys.ps1 = ">>> "
        try:
            sys.ps2
        except AttributeError:
            sys.ps2 = "... "
        cprt = 'Type "help", "copyright", "credits" or "license" for more information.'
        if banner is None:
            self.write("Python %s on %s\n%s\n(%s)\n" %
                       (sys.version, sys.platform, cprt,
                        self.__class__.__name__))
        else:
            self.write("%s\n" % str(banner))
        more = 0
        while 1:
            try:
                if more:
                    prompt = sys.ps2
                else:
                    prompt = sys.ps1
                try:
                    line = self.raw_input(prompt)
                    # Can be None if sys.stdin was redefined
                    encoding = getattr(sys.stdin, "encoding", None)
                    if encoding and not isinstance(line, unicode):
                        line = line.decode(encoding)
                except EOFError:
                    self.write("\n")
                    break
                else:
                    more = self.push(line)
            except KeyboardInterrupt:
                self.write("\nKeyboardInterrupt\n")
                self.resetbuffer()
                more = 0
```

## 手順② インストールしたライブラリをimport

```py
from __future__ import unicode_literals # prompt_toolkitを使うにあたってこの一行が必要です

import sys

from code import InteractiveConsole
from pygments.lexers import PythonLexer
from pygments.styles.monokai import MonokaiStyle

from prompt_toolkit import prompt as input_console
from prompt_toolkit.styles import style_from_pygments
from prompt_toolkit.layout.lexers import PygmentsLexer
```

上記のコードでは`pygments.styles`から**MonokaiStyle**をimportしていますが、各々好きなスタイルをimportしてください。
Pygmentsのスタイル一覧は[こちら](https://help.farbox.com/pygments.html)

## 手順③　コードを書き換える

手順①でコピペしたコードの`line = self.raw_input(prompt)`この部分を、以下のように書き換えます。

```py
line = input_console(
    prompt, lexer=PygmentsLexer(PythonLexer),
    style=style_from_pygments(MonokaiStyle)
)
```

このコードを解説していきます。

### Lexer

```
lexer=PygmentsLexer(PythonLexer),
```

**Lexer**は字句解析のことで、**PythonLexer**を指定することで「このプロンプトではPythonの構文を解析する」ということを指定してます。わざわざPythonLexerを指定していることから、いろんな言語のLexerが存在します。

### style

```
style=style_from_pygments(MonokaiStyle)
```

**style** は文字通りプロンプトのハイライトを、**MonokaiStyle**で指定してます。
PygmentsではMonokaiのほかいろんなstyleがあります。
Pygmentsのスタイル一覧は[こちら](https://help.farbox.com/pygments.html)

## 手順④　継承したクラスを実行する

今まではクラスのコードを書いただけですので、このままではインタプリタは起動しません。

```
if __name__ == "__main__":
    
    console = EnhancedPythonConsole()
    console.interact()
```

これでインタプリタが起動します。

今までのコードを合わせるとこんな感じになります。

```py:enhanced_python.py
#!/usr/bin/env python
# coding: utf-8

from __future__ import unicode_literals

import sys

from code import InteractiveConsole
from pygments.lexers import PythonLexer
from pygments.styles.monokai import MonokaiStyle

from prompt_toolkit import prompt as input_console
from prompt_toolkit.styles import style_from_pygments
from prompt_toolkit.layout.lexers import PygmentsLexer

class EnhancedPythonConsole(InteractiveConsole):

    def interact(self, banner=None):
        """Closely emulate the interactive Python console.

        The optional banner argument specify the banner to print
        before the first interaction; by default it prints a banner
        similar to the one printed by the real Python interpreter,
        followed by the current class name in parentheses (so as not
        to confuse this with the real interpreter -- since it's so
        close!).

        """

        try:
            sys.ps1
        except AttributeError:
            sys.ps1 = ">>> "
        try:
            sys.ps2
        except AttributeError:
            sys.ps2 = "... "
        cprt = 'Type "help", "copyright", "credits" or "license" for more information.'
        if banner is None:
            self.write("Python %s on %s\n%s\n(%s)\n" %
                       (sys.version, sys.platform, cprt,
                        self.__class__.__name__))
        else:
            self.write("%s\n" % str(banner))
        more = 0
        while 1:
            try:
                if more:
                    prompt = sys.ps2
                else:
                    prompt = sys.ps1
                try:

                    line = input_console(
                        prompt, lexer=PygmentsLexer(PythonLexer),
                        style=style_from_pygments(MonokaiStyle)
                    )

                    # Can be None if sys.stdin was redefined
                    encoding = getattr(sys.stdin, "encoding", None)

                    if encoding and not isinstance(line, unicode):
                        line = line.decode(encoding)

                except EOFError:
                    self.write("\n")
                    break

                else:
                    more = self.push(line)

            except KeyboardInterrupt:

                self.write("\nKeyboardInterrupt\n")
                self.resetbuffer()
                more = 0

if __name__ == "__main__":

    console = EnhancedPythonConsole()
    console.interact()
```

## 実際に起動してみる

このコードを`enhanced_python.py`として保存し、実行してみましょう。

```
$ python enhanced_python.py
```

そうすると…

![SnapCrab_NoName_2017-9-22_16-22-21_No-00.png](https://qiita-image-store.s3.amazonaws.com/0/1808/82b1b46f-d9b1-e034-7124-9a816155f50f.png)

このようにインタプリタが起動しました！コードを入力すると…

![SnapCrab_NoName_2017-9-22_16-27-8_No-00.png](https://qiita-image-store.s3.amazonaws.com/0/1808/35a32a42-4031-c541-61fe-63e05046293b.png)

ちゃんとハイライトされています！

## prompt_toolkitについて

インタプリタを拡張するときに使ったprompt_toolkitについてですが、このprompt_toolkitは、Pythonの`raw_input`よりもパワフルなプロンプトを作れるライブラリで、今回のようにプロンプトに入力する文字をハイライトしたり、入力したプロンプトの**履歴を保存**したり、保存した履歴からプロンプトを**サジェスト**したり、色々な機能がありますので、もし興味がありましたらぜひドキュメントを一読してみてください。（全部英語だけど）

* [Python Prompt Toolkit](http://python-prompt-toolkit.readthedocs.io/en/stable/)

prompt_toolkitで拡張したインタプリタの例として、自分が作ったPythonインタプリタのコードがgistにあります。

https://gist.github.com/alice1017/1a87bd6792d027a5ef2b06708d22ca71

今回は自分の環境の都合でPython2.7.5を使いましたが、もちろんPython3でも使えます。
皆さんも自分で好きなようにPythonインタプリタを拡張して使いやすくしましょう！