Welcome, guest | Sign In | My Account | Store | Cart

When you need to inspect Python objects in a human-readable way, you're usually required to implement a custom __str__ or __repr__ which are just boilerplate (e.g., return "Foo(%r, %r, %r)" % (self.bar, self.spam, self.eggs). You may implement __str__ and __repr__ by a base-class, but it's hard to call it inheritance and moreover, you may wish to remove it when you're done debugging.

This simple (yet complete) recipe is a class decorator that injects __str__ and __repr__ into the class being printed. It handles nesting and even cycle detection, allowing you to just plug it into existing classes to get them pretty-printed and perhaps remove it later.

Python, 65 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import threading
from contextlib import contextmanager

_tls = threading.local()

@contextmanager
def _nested():
    _tls.level = getattr(_tls, "level", 0) + 1
    try:
        yield "   " * _tls.level
    finally:
        _tls.level -= 1

@contextmanager
def _recursion_lock(obj):
    if not hasattr(_tls, "history"):
        _tls.history = []  # can't use set(), not all objects are hashable
    if obj in _tls.history:
        yield True
        return
    _tls.history.append(obj)
    try:
        yield False
    finally:
        _tls.history.pop(-1)

def humanize(cls):
    def __repr__(self):
        if getattr(_tls, "level", 0) > 0:
            return str(self)
        else:
            attrs = ", ".join("%s = %r" % (k, v) for k, v in self.__dict__.items())
            return "%s(%s)" % (self.__class__.__name__, attrs)

    def __str__(self):
        with _recursion_lock(self) as locked:
            if locked:
                return "<...>"
            with _nested() as indent:
                attrs = []
                for k, v in self.__dict__.items():
                    if k.startswith("_"):
                        continue
                    if isinstance(v, (list, tuple)) and v:
                        attrs.append("%s%s = [" % (indent, k))
                        with _nested() as indent2:
                            for item in v:
                                attrs.append("%s%r," % (indent2, item))
                        attrs.append("%s]" % (indent,))
                    elif isinstance(v, dict) and v:
                        attrs.append("%s%s = {" % (indent, k))
                        with _nested() as indent2:
                            for k2, v2 in v.items():
                                attrs.append("%s%r: %r," % (indent2, k2, v2))
                        attrs.append("%s}" % (indent,))
                    else:
                        attrs.append("%s%s = %r" % (indent, k, v))
                if not attrs:
                    return "%s()" % (self.__class__.__name__,)
                else:
                    return "%s:\n%s" % (self.__class__.__name__, "\n".join(attrs))

    cls.__repr__ = __repr__
    cls.__str__ = __str__
    return cls

Example:

@humanize
class Foo(object):
    def __init__(self, **kw):
        self.__dict__.update(kw)

x = Foo(bar = 5, zar = "hello", mar = Foo())
x.gar = [Foo(a = 17, b = 18), Foo(c = 19), Foo(e = 20, f = 21)]
x.lap = {
    "zizi" : "tripo",
    "mimi" : Foo(g = 22, h = 23, q = {}, p = []),
    "x" : x,
}
print x

Which results in:

Foo:
   bar = 5
   mar = Foo()
   zar = 'hello'
   gar = [
      Foo:
         a = 17
         b = 18,
      Foo:
         c = 19,
      Foo:
         e = 20
         f = 21,
   ]
   lap = {
      'zizi': 'tripo',
      'mimi': Foo:
         q = {}
         p = []
         g = 22
         h = 23,
      'x': <...>,     # <<< prevent going into a cycle
   }

Possible improvements:

  • Use collections.Mapping and collections.Sequence instead of hard-coded list and dict
  • Customize recursion-lock text
  • Customize indentation
  • Control which attrs are being selected

These are really simple extensions, left out in order to keep the code concise.