なぜ「python pprint alternative」か、なぜその答えはいつも json.dumps か

ずっと苦しい、これ。

要はこれである:

However dicts like d = {1: {'a': 1, 'b': 2}, 2: 22, 3: 33} would look rather ugly, since indent would affect the appearance of dicts with depth greater than 1 as well:
1 MY_DCT = {
2     1: {   'a': 1,
3            'b': 2},
4     #    ^
5     #    |
6     # ugly
7     2: 22,
8     3: 33}

この例は pprint.pformat そのものの出力ではなく pformat の出力を利用した出力ね。自作の例の方がいいか。

まず「データ」を表現したいオブジェクト。何に収納したいかは色々で例えば namedtuple なんぞもいいが、今は単に object として:

 1 class Target(object):
 2     def __init__(self, target, debug):
 3         self.Target = target
 4         self.UseDebugLibrary = debug
 5 
 6 class Project(object):
 7     def __init__(self):
 8         self.confs = {
 9             ("Release", "x64"): {
10                 "Target": Target("hogeX64", False),
11                 },
12             ("Debug", "x64"): {
13                 "Target": Target("hogeX64_d", True),
14                 }
15             }

現実にはこれの何十倍も複雑で、深い階層を持っていると思って欲しい。当然何もしなければ:

1 >>> p = Project()
2 >>> p
3 <__main__.Project object at 0x0000000002BA7E48>

となる。で、「所詮データ収めの場所」の repr 表現が「とってもぷりちぃ」であることは不正じゃないので、これは __repr__ にぷりちぃ表現をブチ込むのはアリだ。少なくとも「アイデンティティ」はデータの中身が主張するのだ。

そういうわけで __repr__ を pformat で書きたいわけだが:

 1 class _Base(object):
 2     def __repr__(self):
 3         from pprint import pformat
 4         return pformat(self.__dict__)
 5 
 6 class Target(_Base):
 7     def __init__(self, target, debug):
 8         self.Target = target
 9         self.UseDebugLibrary = debug
10 
11 class Project(_Base):
12     def __init__(self):
13         self.confs = {
14             ("Release", "x64"): {
15                 "Target": Target("hogeX64", False),
16                 },
17             ("Debug", "x64"): {
18                 "Target": Target("hogeX64_d", True),
19                 }
20             }

まさしく:

U G L Y !!!
1 >>> p = Project()
2 >>> p
3 {'confs': {("Debug", "x64"): {'Target': {'Target': 'hogeX64_d', 'UseDebugLibrary': True}},
4            ("Release", "x64"): {'Target': {'Target': 'hogeX64', 'UseDebugLibrary': False}}}}

引用した stackoverflow のコメントが言う通りなのだが「indent」をどう制御しても、pformat は辞書を延々「折り返さずに」フォーマットする。要するに皆これが大嫌いで「python pprint alternative」という検索を試みる。

そして答えは十中八九「json.dumps を使えぃ」である。ワタシもこれまではイケるならそうしてたんだけれど、今回例にしたのがまさに「json がダメ」なパターン。

2つ問題があるのだが、一つは「プロパティ名は文字列なんだゼ」問題。要するに json の「辞書的なもの」はプロパティセットであって、「キー的なもの」は文字列しか許されない。ので、tuple をキーに使っている今回の例ではそのままでは使えない。

もう一つはどちらかといえば「ダメなので頑張ってこねくりまわそう」とする場合に起こる問題。つまり、「json が読めるような文字列化を施してから json に渡す」としたいわけだが、面倒だから、と eval を介在させようとして起こる。まぁ eval はほんとは使いたくないわけだからいいんだけどさ…。真似して欲しくはないからコードはここには書かないが、要するに「真偽値」が python は True/False、json/javascript は true/false であることが問題になる。(例のように構造がネストしている場合。)

で、Indentation of pformat() output で提案されているものでいいもんはないかなぁ、と思ったが、まぁこれ:

 1 def f(obj_name, given_dct):
 2     """
 3     Converts given dct (body) to a pretty formatted string.
 4     Resulting string used for file writing.
 5 
 6     Args:
 7         obj_name: (str) name of the dict
 8     Returns:
 9         (str)
10     """
11 
12     string = pp.pformat(given_dct, width=1)[1:]
13 
14     new_str = ''
15     for num, line in enumerate(string.split('\n')):
16         if num == 0:
17             # (pprint module always inserts one less whitespace for first line)
18             # (indent=1 is default, giving everything one extra whitespace)
19             new_str += ' '*4 + line + '\n'
20         else:
21             new_str += ' '*3 + line + '\n'
22 
23     return obj_name + ' = {\n' + new_str
24 
25 
26 s = f(obj_name='MY_DCT', given_dct=d)

はそれほど悪くはなかった。ワタシの例だとこんな:

 1 class _Base(object):
 2     def __repr__(self):
 3         from pprint import pformat
 4         pfs = pformat(self.__dict__, width=1)[1:]
 5         new_str = ''
 6         for num, line in enumerate(pfs.split('\n')):
 7             if num == 0:
 8                 # (pprint module always inserts one less whitespace for first line)
 9                 # (indent=1 is default, giving everything one extra whitespace)
10                 new_str += ' ' * 4 + line + '\n'
11             else:
12                 new_str += ' ' * 3 + line + '\n'
13         return '{\n' + new_str
14 
15 class Target(_Base):
16     def __init__(self, target, debug):
17         self.Target = target
18         self.UseDebugLibrary = debug
19 
20 class Project(_Base):
21     def __init__(self):
22         self.confs = {
23             ("Release", "x64"): {
24                 "Target": Target("hogeX64", False),
25                 },
26             ("Debug", "x64"): {
27                 "Target": Target("hogeX64_d", True),
28                 }
29             }
 1 >>> p = Project()
 2 >>> p
 3 {
 4     'confs': {('Debug', 'x64'): {'Target': {
 5        'Target': 'hogeX64_d',
 6        'UseDebugLibrary': True}
 7    },
 8               ('Release', 'x64'): {'Target': {
 9        'Target': 'hogeX64',
10        'UseDebugLibrary': False}
11    }}}
12 
13 >>>

不満がないわけではないが、オリジナルの数億ミリ倍くらいはマシだ。このままでは誤解しかねないインデントではあるので、製品品質にはちと危ないが、少なくとも「(目を凝らせば)読める、読めるぞぉ」なレベルにまでは来てる。

もう一度想像してみて欲しいが、現実の例はあと5~10階層くらいは深い。だって複雑な xml が元になってるものだからな。そうなるとオリジナルではコンソールの横幅を溢れようが気にせず3行にも4行にもなろうが折り返さないので、とても読めたもんじゃなく、「かなりプリント(pretty print)」とはよく言ったもんだ、と思う、ほんとに。それから考えりゃ、ひとまずこの回答は随分救世主だ。(なお、そうであってもこれの一番の問題は、pprint は pure python 版しかないため、場合によっては性能が危ういこと。)