Как вы реализуете __str__ для функции?

Для функции foo:

def foo(x):
     pass

Печать своего представления путем вызова str или repr дает вам что-то скучное:

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

Я хотел бы знать, можно ли переопределить метод __str__ для печати чего-то другого. По сути, я бы хотел:

str(foo)
"I'm foo!'

Теперь я понимаю, что описание функции должно исходить из __doc__ которая является функцией docstring. Однако это всего лишь эксперимент.

При попытке найти решение этой проблемы я столкнулся с внедрением __str__ для classes: как определить метод __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__ для функции?

Ответ 1

Функция в 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__ в любую функцию, просто украсив ее. Это будет выглядеть так:

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

Очевидно, что это имеет некоторые ограничения и недостатки, поскольку вы не можете точно воспроизвести, что def будет делать для новой функции внутри этого декоратора.

Например, использование inspect модуля для поиска аргументов не будет работать должным образом: для вызываемого типа он будет включать в self аргумент self и при использовании универсального декоратора он сможет только сообщать подробности об wrapper. Однако могут быть некоторые решения, например, обсуждаемые в этом вопросе, которые позволят вам восстановить некоторые функциональные возможности.

Но это обычно означает, что вы инвестируете много усилий, чтобы получить работу __str__ над объектом функции, который, вероятно, очень редко будет использоваться. Итак, вы должны подумать о том, действительно ли вам нужна реализация __str__ для ваших функций и какие операции вы будете выполнять над этими функциями.

Ответ 2

Если вы обнаруживаете, что обертываете функции, полезно посмотреть на 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!"