Monday, January 15, 2018

How do you implement __str__ for a function?

Leave a Comment

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?

2 Answers

Answers 1

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.

Answers 2

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!" 
If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment