5

__repr__ is used to return a string representation of an object, but in Python a function is also an object itself, and can have attributes.

How do I set the __repr__ of a function?

I see here that an attribute can be set for a function outside the function, but typically one sets a __repr__ within the object definition itself, so I'd like to set the repr within the function definition itself.


My use case is that I am using tenacity to retry a networking function with exponential backoff, and I want to log the (informative) name of the function I have called last.

retry_mysql_exception_types = (InterfaceError, OperationalError, TimeoutError, ConnectionResetError)


def return_last_retry_outcome(retry_state):
    """return the result of the last call attempt"""
    return retry_state.outcome.result()


def my_before_sleep(retry_state):
    print("Retrying {}: attempt {} ended with: {}\n".format(retry_state.fn, retry_state.attempt_number, retry_state.outcome))


@tenacity.retry(wait=tenacity.wait_random_exponential(multiplier=1, max=1200),
                stop=tenacity.stop_after_attempt(30),
                retry=tenacity.retry_if_exception_type(retry_mysql_exception_types),
                retry_error_callback=return_last_retry_outcome,
                before_sleep=my_before_sleep)
def connect_with_retries(my_database_config):
    connection = mysql.connector.connect(**my_database_config)
    return connection

Currently retry_state.fn displays something like <function <lambda> at 0x1100f6ee0> like @chepner says, but I'd like to add more information to it.

user2864740
  • 60,010
  • 15
  • 145
  • 220
Pranab
  • 2,207
  • 5
  • 30
  • 50

5 Answers5

6

You could use a decorator that returns a class with the __call__ and __repr__ set:

class CustomReprFunc:

    def __init__(self, f, custom_repr):
        self.f = f
        self.custom_repr = custom_repr

    def __call__(self, *args, **kwargs):
        return self.f(*args, **kwargs)

    def __repr__(self):
        return self.custom_repr(self.f)


def set_repr(custom_repr):
    def set_repr_decorator(f):
        return CustomReprFunc(f, custom_repr)
    return set_repr_decorator


@set_repr(lambda f: f.__name__)
def func(a):
    return a


print(repr(func))
Hielke Walinga
  • 2,677
  • 1
  • 17
  • 30
  • Ah you beat me by a few seconds! It might be worth highlighting that you're not actually adding `__repr__` to a function, but rather to a class that wraps the function. – flakes Oct 29 '20 at 19:07
  • Note: This does make the function calls slower. In reality, I'd probably just adjust the point where you're printing it to produce a more friendly string, without changing or wrapping the function. – ShadowRanger Oct 29 '20 at 19:12
  • @flakes Yes correct, the decorator now return a class. So it now an instance of `CustomReprFunc `. – Hielke Walinga Oct 29 '20 at 19:13
  • @ShadowRanger Yes, you can play with CustomReprFunc and also e.g. make a decorator that by default sends `__name__` or just a string. Can save quite a bit of function calls, and you have to remember that function calls in Python are quite slow. – Hielke Walinga Oct 29 '20 at 19:14
  • I'd suggest also using [`functools.update_wrapper`](https://docs.python.org/3/library/functools.html#functools.update_wrapper) to make the class behave more like the actual function (see my answer). – Brian Rodriguez Oct 29 '20 at 19:42
4

It's already set.

>>> repr(lambda x:x)
'<function <lambda> at 0x1100f6ee0>'

The problem is that the function type is immutable, so you can't just assign a new function to function.__repr__, and you also can't create a subtype of function in order to override __repr__. (Not that creating instances of the subclass would be easy, even if it were possible to define it.)

chepner
  • 497,756
  • 71
  • 530
  • 681
  • Weirdly enough, trying to override `.__repr__` doesn't seem to produce an error, although it does not affect the result of `repr()`: `def f(x): return 2*x f.__repr__=lambda:'x -> 2x' repr(f) f.__repr__()` in the python repl shows me `''` for `repr(f)` and `'x -> 2x'` for `f.__repr__()`. – Stef Oct 29 '20 at 18:57
  • 3
    @Stef: `__repr__`, like most special methods, is, partially for performance reasons, looked up on the type, not the instance. So reassigning it on a specific function doesn't do anything (and you can't reassign it on the `function` type). – ShadowRanger Oct 29 '20 at 19:02
4

You can't do this for actual functions; the function type is immutable, and already defines a __repr__, and __repr__ is looked up on the type, not the instance, so changing __repr__ on a given function doesn't change behavior.

While probably not useful in this case, you can make your own callable class (analogous to C++ functors), and those can define their own __repr__. For example:

class myfunction:
    @staticmethod   # Avoids need to receive unused self
    def __call__(your, args, here):
        ... do stuff and return as if it were a function ...

    @classmethod    # Know about class, but again, instance is useless
    def __repr__(cls):
        return f'{cls.__name__}(a, b, c)'

which you could convert to a singleton instance of the class (making it equivalent to a plain function in how you use it) at the end by just doing:

myfunction = myfunction()

to replace the class with a single instance of the class.

Note: In real code, I'd almost certainly just change where I'm printing it to print in a more useful way without modifying the function. This doesn't have much overhead over a plain function or a wrapped plain function (since we put the function itself in __call__ rather than wrapping, making it faster, but requiring a separate class for each "friendly repr function"), but it's just not the job of the function to decide how to represent itself in a human-friendly way; that's your job, based on the situation.

ShadowRanger
  • 143,180
  • 12
  • 188
  • 271
  • I think I prefer Hielke's answer a little bit more, because as a wrapper, the pattern can be repeated more easily. Btw, I've never tried this. Does adding `__call__` as a class method effectively make the class non-instantiable by masking `__init__`? – flakes Oct 29 '20 at 19:16
  • 1
    @flakes: No. `__call__` is invoked when you "call" the instances (that's why you'd do `myfunction = myfunction()` to replace it with an instance of the class). So putting parentheses after the class name invokes `__init__` (after `__new__` if that's defined). Putting them after an instance of the class invokes `__call__`. Fun trivia: If you have a metaclass that defines `__call__`, the metaclass's `__call__` will be invoked before `__new__`/`__init__` because you're calling an instance of the metaclass (the class itself). – ShadowRanger Oct 29 '20 at 19:22
  • @flakes: I agree that Hielke's answer is more flexible. But I also think that it's putting responsibility in the wrong place; if you want a friendly string version of a function, write a single bit of code that does the conversion where you need it, don't make people wrap their functions all over the code base to make them work well with your single consumer. – ShadowRanger Oct 29 '20 at 19:24
  • I generally agree with you, but it's a relatively low cost when considering tenacity is already being wrapped around the method. – flakes Oct 29 '20 at 19:26
  • Also agree that this is more of an x/y problem than anything else. I feel it's more of a "how far can I bend this language?" type question. – flakes Oct 29 '20 at 19:28
4

I think a custom decorator could help:

import functools


class reprable:
    """Decorates a function with a repr method.

    Example:
        >>> @reprable
        ... def foo():
        ...     '''Does something cool.'''
        ...     return 4
        ...
        >>> foo()
        4
        >>> foo.__name__
        'foo'
        >>> foo.__doc__
        'Does something cool.'
        >>> repr(foo)
        'foo: Does something cool.'
        >>> type(foo)
        <class '__main__.reprable'>
    """

    def __init__(self, wrapped):
        self._wrapped = wrapped
        functools.update_wrapper(self, wrapped)

    def __call__(self, *args, **kwargs):
        return self._wrapped(*args, **kwargs)

    def __repr__(self):
        return f'{self._wrapped.__name__}: {self._wrapped.__doc__}'

Demo: http://tpcg.io/uTbSDepz.

Brian Rodriguez
  • 4,250
  • 1
  • 16
  • 37
3

You can change retry_state.fn to retry_state.__name__. I use many decorators like this. If you add a decorator, it will be called each time a function of interest is called.

def display_function(func):
    """ This decorator prints before and after running """

    @functools.wraps(func)
    def function_wrapper(*args, **kwargs):
        print(f'\nNow: Calling {func.__name__}.')
        entity = func(*args, **kwargs)
        print(f'Done: Calling {func.__name__}.\n')
        return entity

    return function_wrapper

Additionally, the retrying module in python allows you to do some of what you're doing by default. I often use a decorator:

import retrying
@retrying.retry(wait_exponential_multiplier=1000, wait_exponential_max=10000)
spen.smith
  • 576
  • 2
  • 16