Pythonで正しく日本語を eval する

これはしたり!ちっとも気がつかなかった!普通のアプリ書いてるとcompile()やeval()を使うことはあんまりないからなぁ。@methaneさんが バグ報告 もしているが、Python2系列は知らね、とばかりにcloseされてしまっている。

>>> print eval("'あいうえお'")
あいうえお
>>> print repr(eval("u'あいうえお'"))
u'\x82\xa0\x82\xa2\x82\xa4\x82\xa6\x82\xa8'
>>> print repr(eval(u"'あいうえお'"))
'\xe3\x81\x82\xe3\x81\x84\xe3\x81\x86\xe3\x81\x88\xe3\x81\x8a'
>>> print eval(u"u'あいうえお'")
あいうえお

うーん。eval()にASCII文字列を渡すとUnicode文字列がASCII文字列のまま、デコードされずに構築されてしまうし、Unicode文字列の場合はソース中のASCII文字列がUTF-8に変換されてしまうようだ。

とりあえず日本語を使ったevalをなんとかしないといけない。先ほどのBug報告ではソースにエンコードを指定するコメントを追加する方法が提案されているが、ソース行が一行増えてしまうとエラー表示がずれたりするし、なんとなくハックっぽい感じもする。

こういう時は astモジュール を使うのが良いだろう。astモジュールを使えばPythonのソースを抽象構文木に変換し、コードを動的に変換できるのである。抽象構文木とか言うとなんか難しそうだが、平たく言えばソースコードの構成要素を、操作しやすいツリー状のデータに変換したものだ。

ここでは、ソースをUnicodeに変換してから抽象構文木を構築し、ASCIIリテラルUTF-8から指定したエンコードに変換すれば良いだろう。

import ast

class _Transform(ast.NodeTransformer):
    def __init__(self, encode):
        super(_Transform, self).__init__()
        self._encode = encode
        
    def visit_Str(self, node):
        if not isinstance(node.s, unicode):
            s = unicode(node.s, 'utf-8')
            node.s = s.encode(self._encode)
        return node

def ueval(src, encode, filename):
    src = unicode(src, encode)
    expr = compile(src, filename, "eval", ast.PyCF_ONLY_AST)
    _Transform(encode).visit(expr.body)
    return eval(compile(expr, filename, "eval"))

使い方はこんな感じになる

>>> print ueval("'あいうえお'+'かきくけこ'", "cp932", "filename")
あいうえおかきくけこ
>>> print ueval("u'あいうえお'+u'かきくけこ'", "cp932", "filename")
あいうえおかきくけこ
>>