How to set a repr for a function itself? [duplicate]
Asked Answered
E

5

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.

Emolument answered 29/10, 2020 at 18:47 Comment(3)
You can print the function's name by using func.__name__Layout
I think a class with a __call__ method would be more appropriate for this use case.Lympho
@Georgy it seems I missed that in my initial search and my question is indeed a duplicate question, however the answers here are so much better.Emolument
L
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.

Lympho answered 29/10, 2020 at 19:17 Comment(4)
This is basically the same as Hielke's older answer (just without the ability to customize the repr on a per-function basis).Illogical
@Illogical yeah, I wrote my answer too slow... :(Lympho
I like the update_wrapper addition.Dzoba
self._wrapped is redundant because functools.update_wrapper automatically adds a __wrapped__ attribute to the wrapper.Bibliotaph
D
8

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))
Dzoba answered 29/10, 2020 at 19:4 Comment(5)
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.Marriageable
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.Illogical
@Marriageable Yes correct, the decorator now return a class. So it now an instance of CustomReprFunc .Dzoba
@Illogical 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.Dzoba
I'd suggest also using functools.update_wrapper to make the class behave more like the actual function (see my answer).Lympho
K
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.)

Kendallkendell answered 29/10, 2020 at 18:51 Comment(2)
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 '<function f at 0x102a8d048>' for repr(f) and 'x -> 2x' for f.__repr__().Intrusive
@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).Illogical
I
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.

Illogical answered 29/10, 2020 at 19:10 Comment(5)
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__?Marriageable
@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).Illogical
@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.Illogical
I generally agree with you, but it's a relatively low cost when considering tenacity is already being wrapped around the method.Marriageable
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.Marriageable
L
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.

Lympho answered 29/10, 2020 at 19:17 Comment(4)
This is basically the same as Hielke's older answer (just without the ability to customize the repr on a per-function basis).Illogical
@Illogical yeah, I wrote my answer too slow... :(Lympho
I like the update_wrapper addition.Dzoba
self._wrapped is redundant because functools.update_wrapper automatically adds a __wrapped__ attribute to the wrapper.Bibliotaph
L
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)
Layout answered 29/10, 2020 at 19:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.