Preface
At some point I was tired of writing & supporting __repr__
methods, so I've decided to write it once and reuse everywhere in my classes.
Since I'm trying to write classes as simple as possible, most of them ends in something like
class MyClass:
def __init__(self, param, other_param, ...):
self.param = param
self.other_param = other_param
...
i.e. initializer parameters names corresponds to fields names, so we can get initializer arguments by simply getattr
'ing corresponding fields.
Main ideas are:
I want to have informative string which includes parameters involved in instance creation. For simple cases it should be possible to copy string & paste in some place (e.g. REPL session) and have similar object definition with as less work as possible. This helps a lot during debugging sessions, logging, especially in failed test cases with randomly generated data.
Great examples are
Counter
&OrderedDict
fromcollections
standard library:>>> from collections import Counter >>> Counter(range(2)) Counter({0: 1, 1: 1}) >>> Counter(range(2)) == Counter({0: 1, 1: 1}) True >>> from collections import OrderedDict >>> OrderedDict.fromkeys(range(2)) OrderedDict([(0, None), (1, None)]) >>> OrderedDict.fromkeys(range(2)) == OrderedDict([(0, None), (1, None)]) True
Once signature change,
__repr__
should handle this automatically for simple cases like renaming/removing/changing order of parameters.
Attempt
Note: following code requires Python3.5+ due to yield from
statement
import inspect
from collections import (OrderedDict,
abc)
from typing import (Any,
Callable,
Iterable,
TypeVar,
Union)
Domain = TypeVar('Domain')
Range = TypeVar('Range')
Map = Callable[[Domain], Range]
Constructor = Callable[..., Domain]
Initializer = Callable[..., None]
def generate_repr(constructor_or_initializer: Union[Constructor, Initializer],
*,
field_seeker: Callable[[Domain, str], Any] = getattr
) -> Map[Domain, str]:
signature = inspect.signature(constructor_or_initializer)
parameters = OrderedDict(signature.parameters)
# remove `self`
parameters.popitem(0)
to_positional_argument_string = repr
to_keyword_argument_string = '{}={!r}'.format
def __repr__(self: Domain) -> str:
return (type(self).__qualname__
+ '(' + ', '.join(to_arguments_strings(self)) + ')')
def to_arguments_strings(object_: Domain) -> Iterable[str]:
for parameter_name, parameter in parameters.items():
field = field_seeker(object_, parameter_name)
if parameter.kind == inspect._VAR_POSITIONAL:
if isinstance(field, abc.Iterator):
yield '...'
else:
yield from map(to_positional_argument_string, field)
elif parameter.kind == inspect._VAR_KEYWORD:
yield from map(to_keyword_argument_string,
field.keys(), field.values())
elif parameter.kind in {inspect._POSITIONAL_ONLY,
inspect._POSITIONAL_OR_KEYWORD}:
yield to_positional_argument_string(field)
else:
yield to_keyword_argument_string(parameter_name, field)
return __repr__
Test
Let's define our class like
class A:
def __init__(self, positional, *variadic_positional, keyword_only,
**variadic_keyword):
self.positional = positional
self.variadic_positional = variadic_positional
self.keyword_only = keyword_only
self.variadic_keyword = variadic_keyword
__repr__ = generate_repr(__init__)
After that
>>> A(1, 2, 3, keyword_only='some', a={'sample': 42}, b={1, 2})
A(1, 2, 3, keyword_only='some', b={1, 2}, a={'sample': 42})
@dataclass
does this for you. It has even been backported to 3.6 \$\endgroup\$