28

Given a function foo:

def foo(x):
     pass

Printing its representation by invoking str or repr gives you something boring like this:

str(foo)
'<function foo at 0x119e0c8c8>'

I'd like to know if it is possible to override a function's __str__ method to print something else. Essentially, I'd like to do:

str(foo)
"I'm foo!'

Now, I understand that the description of a function should come from __doc__ which is the function's docstring. However, this is merely an experiment.

In attempting to figure out a solution to this problem, I came across implementing __str__ for classes: How to define a __str__ method for a class?

This approach involved defining a metaclass with an __str__ method, and then attempting to assign the __metaclass__ hook in the actual class.

I wondered whether the same could be done to the class function, so here's what I tried -

In [355]: foo.__class__
Out[355]: function

In [356]: class fancyfunction(type):
     ...:     def __str__(self):
     ...:         return self.__name__
     ...:     

In [357]: foo.__class__.__metaclass__ = fancyfunction
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)

I figured it wouldn't work, but it was worth a shot!

So, what's the best way to implement __str__ for a function?

cs95
  • 379,657
  • 97
  • 704
  • 746
  • Which methods/functions are called during `str(foo)`? Shouldn't it be `foo.__str__()`? Why can't we simply do `foo.__str__ = lambda : "I'm a foo!"`? – Eric Duminil Nov 23 '17 at 10:26
  • @EricDuminil `__str__` for functions is not writable. – Moses Koledoye Nov 23 '17 at 10:29
  • @MosesKoledoye: Python doesn't complain during `foo.__str__ = lambda : "I'm a foo!"` should the syntax be different because it's a private method? Is the assignment simply ignored? – Eric Duminil Nov 23 '17 at 10:33
  • 2
    @EricDuminil Yes, it won't complain because arbitrary attributes can be bound to function instances. In fact, you can do `f.__str__ = lambda: 6` and `foo.__str__()` returns `6`; the string return value is not enforced. `__str__` is called for functions using hooks via `type(foo)`, and not via the instance `foo`. – Moses Koledoye Nov 23 '17 at 10:38
  • Excellent, thanks. – Eric Duminil Nov 23 '17 at 10:39

2 Answers2

33

A function in Python is just a callable object. Using def to define function is one way to create such an object. But there is actually nothing stopping you from creating a callable type and creating an instance of it to get a function.

So the following two things are basically equal:

def foo ():
    print('hello world')


class FooFunction:
    def __call__ (self):
        print('hello world')

foo = FooFunction()

Except that the last one obviously allows us to set the function type’s special methods, like __str__ and __repr__.

class FooFunction:
    def __call__ (self):
        print('hello world')

    def __str__ (self):
        return 'Foo function'

foo = FooFunction()
print(foo) # Foo function

But creating a type just for this becomes a bit tedious and it also makes it more difficult to understand what the function does: After all, the def syntax allows us to just define the function body. So we want to keep it that way!

Luckily, Python has this great feature called decorators which we can use here. We can create a function decorator that will wrap any function inside a custom type which calls a custom function for the __str__. That could look like this:

def with_str (str_func):
    def wrapper (f):
        class FuncType:
            def __call__ (self, *args, **kwargs):
                # call the original function
                return f(*args, **kwargs)
            def __str__ (self):
                # call the custom __str__ function
                return str_func()

        # decorate with functool.wraps to make the resulting function appear like f
        return functools.wraps(f)(FuncType())
    return wrapper

We can then use that to add a __str__ function to any function by simply decorating it. That would look like this:

def foo_str ():
    return 'This is the __str__ for the foo function'

@with_str(foo_str)
def foo ():
    print('hello world')
>>> str(foo)
'This is the __str__ for the foo function'
>>> foo()
hello world

Obviously, doing this has some limitations and drawbacks since you cannot exactly reproduce what def would do for a new function inside that decorator.

For example, using the inspect module to look at the arguments will not work properly: For the callable type, it will include the self argument and when using the generic decorator, it will only be able to report the details of wrapper. However, there might be some solutions, for example discussed in this question, that will allow you to restore some of the functionality.

But that usually means you are investing a lot of effort just to get a __str__ work on a function object which will probably very rarely be used. So you should think about whether you actually need a __str__ implementation for your functions, and what kind of operations you will do on those functions then.

poke
  • 369,085
  • 72
  • 557
  • 602
  • 5
    Of course, just about everything besides function call functionality will break when you do this. Some of it can be handled manually, like the descriptor protocol. Some of it can sort of be handled, like signature introspection. Some of it is never going to work. – user2357112 Nov 23 '17 at 10:00
  • 1
    @PM2Ring Changed the answer to wrap the final function with `functools.wrapped` so that the essentials are restored :) – poke Nov 23 '17 at 12:52
  • You can remove the need for the `self` argument by making `__call__` a static method (`@staticmethod def __call__(no, self, arg):`). Use `functools.wraps` to restore some function attributes (like the name and annotations). Otherwise, very good answer! – AbyxDev Jan 16 '18 at 04:49
  • @KenHilton `@staticmethod` does the same as ignoring the passing `self` argument (Python will still pass `self` to the function; it’s just that the decorator will skip it for you). There is no real benefit here in wrapping the function *again*. There are already enough levels of wrapping involved here. Also, `__call__` is still an instance method, so even though I don’t need `self` here, making it a static method would feel wrong. – As for `wraps`, I am using that. – poke Jan 19 '18 at 07:55
11

If you find yourself wrapping functions, it's useful to look at functools.partial. It's primarily for binding arguments of course, but that's optional. It's also a class that wraps functions, removing the boilerplate of doing so from scratch.

from functools import partial

class foo(partial):
    def __str__(self):
        return "I'm foo!"

@foo
def foo():
    pass

assert foo() is None
assert str(foo) == "I'm foo!"
A. Coady
  • 54,452
  • 8
  • 34
  • 40