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.
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
andcollections.Sequence
instead of hard-codedlist
anddict
- 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.