Skip to main content
edited tags
Link
200_success
  • 144.2k
  • 22
  • 188
  • 473
Tweeted twitter.com/StackCodeReview/status/1098236004656443393
Source Link

Simple generic auto __repr__

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:

  1. 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 from collections 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
    
  2. 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})