Python2.xでは __str__ が unicode を返しても良い

ソースはこれ: http://mail.python.org/pipermail/python-dev/2006-December/070238.html

例えばパスをwrapするオブジェクトを作るときに、パスをunicodeで扱うのであれば、その文字列表現もunicodeにしたい。
そのオブジェクトに文字列表現を与えるとすると

class FooPath(object):
    def __init__(self, path):
        self.path = path

    def __str__(self):
        return "FooPath('%s')" % (self.path,)

self.path が unicode だと、この __str__() の結果は unicode になってしまう。

In [9]: foo = FooPath(u'foobar')

In [10]: foo.__str__()
Out[10]: u"FooPath('foobar')"

Bazaarのソースで、 __str__() が unicode を返すのを嫌って、次のようになっていた。

class BarPath(object):
    def __init__(self, path):
        assert isinstance(path, unicode)
        self.path = path

    def __str__(self):
        return "BarPath('%s')" % (self.path.encode('utf-8'),)

    def __unicode__(self):
        return u"BarPath('%s')" % (self.path,)

しかし、この方式では以下の問題がある。

  • print barpath としたときに、sys.stdout.encodingを無視して utf-8 が使われる。
  • "%s" % (barpath,) としたときに、ログの出力先のエンコーディングを無視して utf-8 が使われる。 log.warn('%s', barpath) も同じ

後者に関しては u'%s' を使うようにすれば良いが、 "%s" % (u"foo") の結果がunicodeになるのに比べると統一感が無い。
前者に関してはそもそも回避方法が無い。

そこで、 __str__() が unicode を返してもいいものかどうかが悩みどころなんだけれども、上述のとおり良いらしい。ただし、次のような問題が起こるので気をつけないといけない。

In [16]: foo = FooPath(u'\u0342')

In [19]: "%s" % (foo,)
Out[19]: u"FooPath('\u0342')"

In [20]: s = str(foo)
---------------------------------------------------------------------------
UnicodeEncodeError                        Traceback (most recent call last)

この UnicodeEncodeError で誰が悪いのかというと、 unicode かもしれないものに対して str() を呼び出した側が悪い。
str(u"\u0342") もエラーを出すからね。repr()と違って、str()に関してはオブジェクト側が成功する責任を負っていない。

でも、やっぱり既存のコードに手を加えるのには勇気が要るなぁ。

このブログに乗せているコードは引用を除き CC0 1.0 で提供します。