如何为一个函数实现__str__方法?

29

假设有一个名为 foo 的函数:

def foo(x):
     pass

通过调用strrepr打印其表示形式会得到一些无聊的东西,例如:

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

我想知道是否可以重写一个函数的__str__方法以打印其他东西。基本上,我想做的是:

str(foo)
"I'm foo!'

现在,我明白函数的描述应该来自函数的文档字符串__doc__。然而,这只是一个实验。

在尝试解决这个问题时,我发现可以为实现__str__如何为类定义__str__方法?

该方法涉及定义一个具有__str__方法的元类,然后尝试在实际类中分配__metaclass__钩子。

我想知道是否可以对function类做同样的事情,所以我尝试了以下操作 -

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)

我认为它不会起作用,但值得一试!

那么,如何最佳实现函数的__str__方法呢?


str(foo) 过程中会调用哪些方法/函数?难道不应该是 foo.__str__() 吗?为什么我们不能简单地执行 foo.__str__ = lambda : "I'm a foo!" - Eric Duminil
@EricDuminil 函数的 __str__ 不可写。 - Moses Koledoye
@MosesKoledoye:Python在foo.__str__ = lambda: "I'm a foo!"期间不会抱怨,因为它是一个私有方法,所以语法应该有所不同吗?这个赋值是被简单地忽略了吗? - Eric Duminil
2
@EricDuminil 是的,它不会抱怨,因为任意属性可以绑定到函数实例。事实上,您可以执行 f.__str__ = lambda: 6foo.__str__() 返回 6;字符串返回值不是强制执行的。使用 type(foo) 钩子调用函数的 __str__,而不是通过实例 foo 调用。 - Moses Koledoye
非常好,谢谢。 - Eric Duminil
2个回答

33

Python中的函数只是可调用对象。使用def定义函数是创建这种对象的一种方式,但实际上你也可以创建一个可调用类型并创建它的实例来获取函数。

因此,以下两种方法基本上是相等的:

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


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

foo = FooFunction()

除了最后一个,它显然允许我们设置函数类型的特殊方法,例如__str____repr__

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

    def __str__ (self):
        return 'Foo function'

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

但是为此创建一个特定类型会有点繁琐,而且这也会使理解函数的过程更加困难:毕竟,def语法允许我们仅仅定义函数体。因此,我们希望保持这种方式!

幸运的是,Python拥有一个称为装饰器的伟大特性,我们可以在这里使用它。我们可以创建一个函数装饰器,将任何函数包装在一个自定义类型中,该类型调用自定义函数来执行__str__。可能看起来像这样:

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

我们可以通过简单的装饰器将这个功能添加到任何函数中,从而为其添加一个__str__函数。代码如下:

```python @add_str_function def my_function(): # Function code here... ```
```add_str_function```是一个可以添加```__str__```方法的装饰器函数。
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

显然,这样做存在一些限制和缺点,因为您无法完全复制装饰器内的新函数所执行的操作。

例如,使用inspect模块查看参数将无法正常工作:对于可调用类型,它将包括self参数,而在使用通用装饰器时,它只能报告wrapper的详细信息。但是,可能会有一些解决方案,例如在这个问题中讨论的,它将允许您恢复一些功能。

但这通常意味着您正在投入很多精力来使一个函数对象上的__str__工作,而这个函数对象可能很少被使用。因此,您应该考虑是否实际需要为您的函数实现__str__,以及那些函数将要执行的操作的类型。


5
当然,除了函数调用功能之外,几乎所有的东西都会在你这样做时出现问题。其中一些可以手动处理,例如描述符协议。其中一些可以部分地处理,例如签名内省。有些永远不会起作用。 - user2357112
1
@PM2Ring将答案更改为使用functools.wrapped包装最终函数,以便恢复必要的内容 :) - poke
你可以通过将__call__定义为静态方法(@staticmethod def __call__(no, self, arg):)来消除对self参数的需求。使用functools.wraps来恢复一些函数属性(如名称和注释)。否则,非常好的答案! - AbyxDev
@KenHilton @staticmethod 的作用与忽略传递的 self 参数相同(Python 仍会将 self 传递给函数;只是装饰器会为您跳过它)。在这里再次包装函数没有真正的好处。已经涉及到足够多的包装级别了。此外,__call__ 仍然是一个实例方法,因此即使我在这里不需要 self,将其设置为静态方法也会感觉不对。-至于 wraps,我正在使用它。 - poke

11

如果你发现自己经常要封装函数,那么看一下 functools.partial 会很有用。当然,它主要是用来绑定参数的,但这是可选的。它还是一个可以封装函数的类,从头开始消除了样板文件。

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!"

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接