Метод __call__: от объектно-ориентированного программирования к функциональному стилю

Раздел: Python -> Объектно-ориентированное программирование

Основные концепции и варианты использования метода __call__

Метод __call__ в Python позволяет сделать объект класса вызываемым, как обычную функцию. Для этого достаточно определить метод __call__ в классе. Такой объект может сохранять состояние между вызовами, что открывает широкие возможности для создания гибких конструкций.

Базовое решение: класс-счетчик

Самый простой пример - класс, который подсчитывает количество вызовов:

class Counter:
    def __init__(self):
        self.count = 0
    def __call__(self):
        self.count += 1
        return self.count

c = Counter()
print(c())  # 1
print(c())  # 2

атрибуты класса python (атрибуты классов и объектов в python)

Пояснение: каждый вызов c() увеличивает внутренний счётчик и возвращает новое значение. Это демонстрирует ключевую особенность __call__ - возможность сохранять и изменять состояние.

Типичные ошибки и проблемы:

  • Забыть определить __init__ - тогда атрибут count не будет проинициализирован, и возникнет AttributeError.
  • Если требуется передавать аргументы, их необходимо явно объявить в сигнатуре __call__, иначе TypeError.

Решение: всегда инициализировать все используемые атрибуты в __init__ и явно прописывать параметры __call__.

Как реализовать декоратор в виде класса?

Класс-декоратор принимает функцию в __init__ и предоставляет __call__, который оборачивает вызов.

import time

class Timer:
    def __init__(self, func):
        self.func = func
    def __call__(self, *args, **kwargs):
        start = time.time()
        result = self.func(*args, **kwargs)
        end = time.time()
        print(f'Время выполнения: {end - start:.4f} сек')
        return result

@Timer
def compute():
    return sum(range(10**6))

print(compute())

библиотека классов python (библиотека классов в python)

Пояснение: декоратор Timer измеряет время работы функции. При использовании с синтаксисом @Timer Python автоматически создаёт экземпляр класса и передаёт функцию.

Проблемы:

  • Потеря метаданных исходной функции (имя, строка документации). Можно использовать functools.wraps внутри __call__.
  • Если декоратор нужно применять с аргументами, требуется дополнительный уровень вложенности.

Решение: для сохранения метаданных применить @functools.wraps(self.func) в методе __call__ или использовать библиотеку wrapt.

Цель использования: логирование, профилирование, ограничение доступа, трансформация результатов.

Как создать объект, запоминающий аргументы при инициализации?

Такие объекты реализуют частичное применение функции.

class Partial:
    def __init__(self, func, *args):
        self.func = func
        self.args = args
    def __call__(self, *more_args):
        return self.func(*self.args, *more_args)

def multiply(a, b, c):
    return a * b * c

partial_mult = Partial(multiply, 2, 3)
print(partial_mult(4))  # 24

метод объекта python (методы объектов в python)

Пояснение: при создании экземпляра сохраняются первые аргументы. При вызове они объединяются с новыми.

Проблемы:

  • Не обрабатываются именованные аргументы. Для полной поддержки нужно принимать и передавать **kwargs.
  • Очередность аргументов: сохранённые всегда идут первыми - это подходит только для позиционных параметров.

Решение: дополнить метод __call__ параметрами **kwargs и передавать их в целевую функцию.

Цель использования: создание специализированных функций из общей, адаптация API, частичное применение.

Как реализовать каррирование с помощью __call__?

Каррирование - преобразование функции от нескольких аргументов в цепочку функций от одного аргумента.

class Curry:
    def __init__(self, func, *args):
        self.func = func
        self.args = args
    def __call__(self, *more_args):
        combined = self.args + more_args
        # Упрощённое определение количества аргументов
        if len(combined) >= self.func.__code__.co_argcount:
            return self.func(*combined)
        else:
            return Curry(self.func, *combined)

def add(a, b, c):
    return a + b + c

curried = Curry(add)
print(curried(1)(2)(3))  # 6

метод call python (метод __call__ в python)

Пояснение: при недостаточном количестве аргументов возвращается новый экземпляр Curry, накапливающий аргументы. Вызов продолжается, пока не наберётся полное количество.

Проблемы:

  • Не учитываются *args и **kwargs целевой функции - в примере используется __code__.co_argcount, что не работает для функций с переменным числом аргументов.
  • Ошибка при неправильном порядке аргументов.

Решение: для надёжного определения используйте inspect.signature и обрабатывайте все виды параметров.

Цель использования: стиль функционального программирования, создание частично применённых функций.

Как сохранять результаты вызова для повторного использования (мемоизация)?

class Memoize:
    def __init__(self, func):
        self.func = func
        self.cache = {}
    def __call__(self, *args):
        if args not in self.cache:
            self.cache[args] = self.func(*args)
        return self.cache[args]

@Memoize
def fib(n):
    if n < 2:
        return n
    return fib(n-1) + fib(n-2)

print(fib(35))  # 9227465

Python структура объекта (структура объекта в python)

Пояснение: результаты вызовов кэшируются по кортежу аргументов. Последующие вызовы с теми же аргументами возвращают сохранённое значение.

Проблемы:

  • Кэш неограниченно растёт - память может переполниться.
  • Аргументы должны быть хэшируемыми (нельзя использовать списки или словари).
  • Не работает с именованными аргументами.

Решение: добавить ограничение размера кэша (LRU) и поддержку kwargs (преобразовывать в frozenset).

Цель использования: ускорение рекурсивных вычислений, дорогих операций, чтения из базы данных.

Как объединить несколько функций в цепочку (композиция)?

class Compose:
    def __init__(self, *funcs):
        self.funcs = funcs
    def __call__(self, x):
        for f in self.funcs:
            x = f(x)
        return x

def double(x): return x * 2
def inc(x): return x + 1

composed = Compose(double, inc)
print(composed(3))  # 7 (3*2+1)

Пояснение: каждый вызов последовательно применяет функции из кортежа. Порядок слева направо.

Проблемы:

  • Отсутствие обработки ошибок - если одна функция упадёт, цепочка прервётся.
  • Нельзя передавать дополнительные аргументы между вызовами.

Решение: реализовать try/except внутри цикла или передавать контекст через кортеж.

Цель использования: конвейерная обработка данных, построение пайплайнов, DRY-принцип.

- Object attribute python (атрибуты объекта в python)
- Python call method (вызов метода в python)
- Python класс данных (класс данных в python)

Продвинутые примеры использования __call__

1. LRU-кэш с ограниченным размером

Реализация кэша с вытеснением наименее используемых записей (LRU) на основе OrderedDict.

Пример
from collections import OrderedDict

class LRUCache:
    def __init__(self, func, maxsize=128):
        self.func = func
        self.maxsize = maxsize
        self.cache = OrderedDict()
    def __call__(self, *args, **kwargs):
        key = (args, tuple(sorted(kwargs.items())))
        if key in self.cache:
            self.cache.move_to_end(key)
            return self.cache[key]
        result = self.func(*args, **kwargs)
        self.cache[key] = result
        if len(self.cache) > self.maxsize:
            self.cache.popitem(last=False)
        return result

@LRUCache
def power(base, exp):
    return base ** exp

print(power(2, 10))  # 1024 (вычислено)
print(power(2, 10))  # 1024 (из кэша)
1024
1024

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

2. Многоуровневый вызов (chaining) для построения запросов

Класс, который при вызове возвращает новый callable объект, накапливающий параметры.

Пример
class QueryBuilder:
    def __init__(self, table=''):
        self.table = table
    def __call__(self, table=None):
        if table:
            return QueryBuilder(table)
        return self
    def where(self, condition):
        return QueryBuilder(f'{self.table} WHERE {condition}')
    def order_by(self, field):
        return QueryBuilder(f'{self.table} ORDER BY {field}')
    def __str__(self):
        return f'SELECT * FROM {self.table}' if self.table else ''

query = QueryBuilder()('users').where('age > 18').order_by('name')
print(str(query))  # SELECT * FROM users WHERE age > 18 ORDER BY name
SELECT * FROM users WHERE age > 18 ORDER BY name

Пояснение: первый вызов ('users') создаёт экземпляр с таблицей. Методы возвращают новый экземпляр с обновлённым запросом. Это напоминает fluent-интерфейсы.

3. Прокси для функций с логированием аргументов

Обёртка, которая логирует все вызовы и сохраняет метаданные исходной функции.

Пример
import functools

class LogProxy:
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
    def __call__(self, *args, **kwargs):
        print(f'Вызов {self.func.__name__} с аргументами {args}, {kwargs}')
        return self.func(*args, **kwargs)

@LogProxy
def greet(name, greeting='Hello'):
    return f'{greeting}, {name}!'

print(greet('Alice'))
print(greet('Bob', greeting='Hi'))
Вызов greet с аргументами ('Alice',), {}
Hello, Alice!
Вызов greet с аргументами ('Bob',), {'greeting': 'Hi'}
Hi, Bob!

Пояснение: functools.update_wrapper копирует имя, документацию и другие атрибуты. Прокси ведёт себя как оригинальная функция, но с дополнительным логированием.

4. Асинхронный вызываемый объект

Метод __call__ может быть корутиной, если класс предназначен для асинхронного контекста.

Пример
import asyncio

class AsyncCounter:
    def __init__(self):
        self.count = 0
    async def __call__(self):
        await asyncio.sleep(0.1)
        self.count += 1
        return self.count

async def main():
    counter = AsyncCounter()
    print(await counter())
    print(await counter())

asyncio.run(main())
1
2

Пояснение: объявление async def __call__ делает экземпляр await-совместимым. Такие объекты удобно использовать в асинхронном коде на месте функций.

5. Callable объект с управлением через свойства

Комбинация __call__ и @property для создания объектов с настраиваемым поведением.

Пример
class TemperatureConverter:
    def __init__(self, scale='C'):
        self.scale = scale
    def __call__(self, value):
        if self.scale == 'C':
            return (value - 32) * 5/9  # Fahrenheit to Celsius
        else:
            return value * 9/5 + 32    # Celsius to Fahrenheit
    @property
    def to_celsius(self):
        return TemperatureConverter('C')
    @property
    def to_fahrenheit(self):
        return TemperatureConverter('F')

converter = TemperatureConverter('C')
print(converter(100))        # 100°F -> 37.78°C
print(converter.to_fahrenheit(0))  # 0°C -> 32°F
37.77777777777778
32.0

Пояснение: свойства возвращают новый экземпляр с изменённой шкалой. Сам объект вызывается как функция преобразования. Это пример гибридного дизайна.

6. Реализация паттерна «Одиночка» (Singleton) через __call__

Метакласс или декоратор класса, но можно также использовать callable объект, который всегда возвращает один и тот же экземпляр.

Пример
class Singleton:
    _instance = None
    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance
    def __init__(self, name):
        self.name = name
    def __call__(self, new_name=None):
        if new_name:
            self.name = new_name
        return self.name

a = Singleton('Alpha')
b = Singleton('Beta')
print(a is b)        # True
print(a())           # Beta (последнее значение)
print(b('Gamma'))    # Gamma
print(a())           # Gamma
True
Beta
Gamma
Gamma

Пояснение: механизм __new__ гарантирует единственный экземпляр. Метод __call__ позволяет изменять атрибуты и возвращать их, словно функция.

метод __call__ в Python - comments

En
метод call python (python)