Метод __call__: от объектно-ориентированного программирования к функциональному стилю
Основные концепции и варианты использования метода __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)) # 9227465Python структура объекта (структура объекта в 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-принцип.
Продвинутые примеры использования __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__ позволяет изменять атрибуты и возвращать их, словно функция.