Функторы

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

Функторы – это объекты классов, которые можно выполнять как функции.

Например, создадим класс счетчик:

class Counter:
    def __init__(self):
        self.__counter = 0

И далее, положим, что мы хотим вызывать его экземпляр:

c1 = Counter()

как функцию, то есть:

c1()

Для этого нужно выполнить перегрузку оператора () с помощью реализации метода __call__. Пусть он у нас будет таким:

def __call__(self, *args, **kwargs):
    self.__counter += 1
    print( self.__counter )
    return self.__counter

Теперь, при каждом вызове этого класса в виде функции, мы будем попадать в метод call и увеличивать счетчик на 1:

c1()
c1()

Также можно добавить еще один экземпляр класса, который будет работать совершенно независимо от первого:

c2 = Counter()
c2()
c2()

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

class StripChars:
    def __init__(self, chars):
        self.__chars = chars
 
    def __call__(self, *args, **kwargs):
        if not isinstance(args[0], str):
            raise ValueError("Аргумент должен быть строкой")
 
        return args[0].strip(self.__chars)

И вызовем его как функцию:

s1 = StripChars("?:!.; ")
print( s1(" Hello World! ") )

На выходе получаем строку с удаленными символами, перечисленные в конструкторе. Но это опять же лишь пример функтора в виде класса. Если такая (подобная) задача встречается на практике, то ее, как правило, реализуют через замыкания, то есть, создают функцию с определением внутри нее еще одной функции:

def StripChars(chars):
    def stringStrip(string):
        if not isinstance(string, str):
            raise ValueError("Аргумент должен быть строкой")
 
        return string.strip(chars)
    return stringStrip

И, далее, следует аналогичный вызов:

s1 = StripChars("?:!.; ")
print( s1(" Hello World! ") )

Только теперь переменная s1 ссылается не на экземпляр класса, а на функцию stringStrip, которая возвращается функцией StripChars. Причем, в момент ее вызова создается контекст выполнения данной функции, фактически новый объект, и мы ссылаемся на функцию внутри этого объекта:

Сама же внутренняя функция stringStrip имеет ссылку на внешний контекст, то есть, на объект, созданный StipChars. Поэтому сборщик мусора не уничтожает его.

Из этой иллюстрации хорошо видно, что если создать еще один такой же объект:

s2 = StripChars("?:!.; ")
print( s2(" Hello? ") )

то они будут работать совершенно независимо, т.к. функция stringStrip также динамически создается внутри контекста выполнения функции StipChars. Если, например, вывести их id:

print( id(s1), id(s2), sep="\n")

то они будут совершенно разными.

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