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!"
0 comments:
Post a Comment