В Python скобки после имени — это не просто синтаксис вызова функции. Скобки означают: «выполни этот объект». Когда вы пишете some_function(42), Python ищет у объекта some_function специальный метод, который определяет, что должно произойти при таком вызове.
Для обычных функций этот механизм скрыт внутри интерпретатора. Но для объектов пользовательских классов вы можете определить его сами — именно для этого существует метод __call__.
Прежде чем двигаться дальше, необходимо разобрать одну путаницу, которая почти неизбежно возникает на этом этапе.
Когда вы пишете obj = MyClass(), вы тоже используете скобки — и здесь тоже задействован __call__, но на другом уровне. Класс в Python — это тоже объект, и у него есть свой __call__, который при вызове запускает __new__ и __init__.
Этот механизм управляется метаклассами — «классами для классов». Но сейчас это для нас избыточная сложность.
В рамках этого урока важно лишь понять управление созданием объектов — это отдельный уровень.
Сейчас нас интересует другая ситуация: объект уже создан, и мы хотим, чтобы его можно было вызвать так же, как вызывают функцию.
obj = MyClass() # вызов класса — создание объекта, другой механизм
obj() # вызов экземпляра — именно это определяет наш __call__Разница принципиальная: в первом случае мы создаём объект, во втором — выполняем уже существующий объект. Это один и тот же синтаксис, но совершенно разная семантика.
Начнём с абсолютного минимума — класс, в котором __call__ просто выводит сообщение. Никакой логики, только демонстрация самого факта вызова:
class Greeter:
def __call__(self):
# Этот метод выполняется каждый раз, когда объект вызывают со скобками
print("Привет!")
g = Greeter() # создаём объект
g() # вызываем объект — Python автоматически вызывает __call__
g() # можно вызывать сколько угодно разВывод:
Привет!
Привет!
Python не требует явного вызова g.__call__() — он делает это автоматически при виде скобок после имени объекта. Вы можете убедиться, что это один и тот же механизм:
g() # синтаксический сахар
g.__call__() # явный вызов — результат идентичен__call__ может принимать любые аргументы — точно так же, как обычная функция:
class Adder:
def __call__(self, a, b):
# Аргументы передаются прямо в __call__
return a + b
add = Adder()
print(add(3, 7)) # 10
print(add(10, 20)) # 30На этом этапе объект add ведёт себя неотличимо от обычной функции с точки зрения вызывающего кода. И это именно то, что нам нужно — но пока непонятно, зачем. Ответ на этот вопрос — в следующем разделе.
Обычные функции не имеют памяти между вызовами. Каждый вызов начинается с чистого листа, и любое состояние, которое нужно сохранить, приходится передавать явно или хранить в глобальных переменных.
Замыкания решают эту проблему частично, но у них есть ограничения: их сложно расширять, добавлять методы и масштабировать.
Класс с __call__ объединяет два мира: объект хранит состояние и конфигурацию, а __call__ определяет, что происходит при его вызове.
Покажем разницу на конкретном примере. Предположим, нам нужен счётчик, который можно вызывать как функцию.
Вариант с замыканием:
def make_counter():
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
c = make_counter()
print(c()) # 1
print(c()) # 2
# Сбросить счётчик или получить текущее значение без вызова — непросто.
# Придётся возвращать несколько функций или использовать изменяемый контейнер.Вариант с классом и __call__:
class Counter:
def __init__(self, start=0, step=1):
self.count = start # начальное значение счётчика
self.step = step # шаг увеличения
def __call__(self):
# При вызове увеличиваем счётчик на шаг и возвращаем текущее значение
self.count += self.step
return self.count
def reset(self):
# Дополнительный метод — с замыканием это было бы неудобно
self.count = 0
def peek(self):
# Получить текущее значение без увеличения
return self.count
c = Counter(start=0, step=2)
print(c()) # 2
print(c()) # 4
print(c.peek()) # 4 — текущее значение без вызова
c.reset()
print(c.peek()) # 0 — сброшенКласс явно выигрывает, как только логика становится сложнее одного действия. Параметры конфигурации хранятся в атрибутах, дополнительные операции реализуются как методы, и всё это — часть одного связного объекта.
Проверить, является ли объект вызываемым, можно с помощью встроенной функции callable():
print(callable(c)) # True — у Counter определён __call__
print(callable(Counter)) # True — класс тоже вызываем (создание объекта)
print(callable(42)) # False — число не вызываемо
print(callable(print)) # True — встроенная функцияОдна из наиболее ценных возможностей, которую открывает __call__ — создание объектов с конфигурацией. Вы создаёте объект с нужными параметрами, а затем используете его как функцию в любом месте программы.
class Multiplier:
def __init__(self, factor):
# factor — множитель, задаётся один раз при создании объекта
self.factor = factor
def __call__(self, x):
return x * self.factor
double = Multiplier(2)
triple = Multiplier(3)
ten_times = Multiplier(10)
print(double(5)) # 10
print(triple(5)) # 15
print(ten_times(5)) # 50
# Объект можно передавать туда, где ожидается функция
numbers = [1, 2, 3, 4, 5]
print(list(map(double, numbers))) # [2, 4, 6, 8, 10]
print(list(map(triple, numbers))) # [3, 6, 9, 12, 15]Каждый объект — отдельная конфигурация одной и той же логики. Это особенно удобно, когда одну и ту же операцию нужно выполнять с разными параметрами в разных частях приложения.
Одно из важнейших практических применений __call__ — реализация декораторов на основе классов.
Вы уже работали с декораторами как с функциями.
Теперь разберём, как тот же механизм реализуется через класс, и почему это часто удобнее.
Напомним структуру декоратора-функции:
def logger(func):
def wrapper(*args, **kwargs):
print(f"Вызов функции {func.__name__}")
result = func(*args, **kwargs)
print(f"Функция {func.__name__} завершена")
return result
return wrapper
@logger
def process_payment(amount):
print(f"Обрабатываем платёж: {amount} руб.")
return TrueКогда Python видит декоратор @logger, он выполняет: process_payment = logger(process_payment). После этого process_payment указывает не на исходную функцию, а на wrapper.
Теперь реализуем тот же декоратор через класс:
class Logger:
def __init__(self, func):
# При декорировании Python передаёт функцию в __init__.
# Мы сохраняем её как атрибут объекта.
self.func = func
self.call_count = 0 # счётчик вызовов — с замыканием это было бы неудобно
def __call__(self, *args, **kwargs):
# __call__ выполняется каждый раз, когда вызывают задекорированную функцию.
self.call_count += 1
print(f"[LOG] Вызов {self.func.__name__} (всего вызовов: {self.call_count})")
result = self.func(*args, **kwargs)
print(f"[LOG] {self.func.__name__} завершена, результат: {result}")
return result
@Logger
def process_payment(amount):
print(f"Обрабатываем платёж: {amount} руб.")
return TrueРазберём, что происходит по шагам:
# Шаг 1: Python видит @Logger и выполняет:
process_payment = Logger(process_payment)
# Теперь process_payment — это объект класса Logger,
# а исходная функция сохранена в self.func
# Шаг 2: Вызов задекорированной функции:
process_payment(1500)
# Python видит скобки и вызывает __call__ объекта LoggerВывод:
[LOG] Вызов process_payment (всего вызовов: 1)
Обрабатываем платёж: 1500 руб.
[LOG] process_payment завершена, результат: True
process_payment(2000)
[LOG] Вызов process_payment (всего вызовов: 2)
Обрабатываем платёж: 2000 руб.
[LOG] process_payment завершена, результат: True
Ключевое преимущество над декоратором-функцией: счётчик call_count хранится в объекте и доступен напрямую:
print(process_payment.call_count) # 2С замыканием получить доступ к внутренней переменной счётчика было бы значительно сложнее.
Когда декоратор принимает параметры, структура на основе функций становится трёхуровневой и трудно читаемой. Класс решает эту проблему элегантно.
Сравним оба подхода на реальном примере — декораторе, проверяющем роль пользователя в веб-приложении:
Декоратор-функция с параметром:
def require_role(role):
def decorator(func):
def wrapper(user, *args, **kwargs):
if user.get("role") != role:
raise PermissionError(f"Требуется роль: {role}")
return func(user, *args, **kwargs)
return wrapper
return decoratorТри уровня вложенности: require_role → decorator → wrapper. Это работает, но при усложнении логики становится трудно читаемым.
Тот же декоратор через класс:
class RequireRole:
def __init__(self, role):
# Параметр сохраняется при создании объекта-декоратора
self.role = role
def __call__(self, func):
# __call__ вызывается в момент декорирования — получает функцию
# и возвращает обёртку
def wrapper(user, *args, **kwargs):
if user.get("role") != self.role:
raise PermissionError(
f"Доступ запрещён. Требуется роль: {self.role!r}, "
f"у пользователя: {user.get('role', 'не задана')!r}"
)
return func(user, *args, **kwargs)
return wrapperИспользование в обоих случаях одинаково:
@RequireRole("admin")
def delete_user(user, user_id):
print(f"Пользователь {user_id} удалён")
return True
@RequireRole("moderator")
def ban_user(user, user_id):
print(f"Пользователь {user_id} заблокирован")
return TrueПроверим работу:
admin_user = {"id": 1, "name": "Alice", "role": "admin"}
regular_user = {"id": 2, "name": "Bob", "role": "user"}
delete_user(admin_user, 99)
# Пользователь 99 удалён
try:
delete_user(regular_user, 99)
except PermissionError as e:
print(e)
# Доступ запрещён. Требуется роль: 'admin', у пользователя: 'user'
try:
ban_user(admin_user, 99)
except PermissionError as e:
print(e)
# Доступ запрещён. Требуется роль: 'moderator', у пользователя: 'admin'Важно разобрать последовательность вызовов при использовании декоратора с параметрами:
@RequireRole("admin")
def delete_user(user, user_id): ...
# Шаг 1: RequireRole("admin") → создаётся объект, self.role = "admin"
# Шаг 2: объект вызывается с функцией delete_user → __call__(delete_user)
# → возвращается wrapper
# Шаг 3: delete_user = wrapper
#
# При вызове delete_user(admin_user, 99):
# Шаг 4: выполняется wrapper(admin_user, 99)В этом случае __call__ вызывается один раз — в момент декорирования, а не при каждом вызове функции. Это принципиально отличается от декоратора без параметров, где __call__ выполняется при каждом вызове задекорированной функции.
Рассмотрим пример, который напрямую встречается в веб-разработке — ограничитель частоты запросов (rate limiter). Это компонент, который не позволяет одной и той же операции выполняться слишком часто. Его используют для защиты API от злоупотреблений, ограничения попыток входа в систему и контроля нагрузки на внешние сервисы.
import time
class RateLimiter:
"""
Декоратор-ограничитель частоты вызовов.
Разрешает не более max_calls вызовов в период period секунд.
При превышении лимита выбрасывает исключение.
"""
def __init__(self, max_calls, period):
self.max_calls = max_calls # максимальное число вызовов
self.period = period # период в секундах
self.calls = [] # список временных меток вызовов
def __call__(self, func):
# __call__ вызывается при декорировании — получает функцию
def wrapper(*args, **kwargs):
now = time.time()
# Убираем из истории вызовы, которые вышли за пределы периода
self.calls = [t for t in self.calls if now - t < self.period]
if len(self.calls) >= self.max_calls:
raise RuntimeError(
f"Превышен лимит: не более {self.max_calls} вызовов "
f"за {self.period} сек. Повторите попытку позже."
)
# Фиксируем текущий вызов
self.calls.append(now)
return func(*args, **kwargs)
return wrapper
# Разрешаем не более 3 запросов в 10 секунд
@RateLimiter(max_calls=3, period=10)
def send_email(address, subject):
print(f"Письмо отправлено: {address} — {subject}")
return True
# Первые три вызова проходят
send_email("alice@example.com", "Подтверждение заказа")
send_email("bob@example.com", "Сброс пароля")
send_email("carol@example.com", "Добро пожаловать")
# Четвёртый вызов в течение 10 секунд — исключение
try:
send_email("dave@example.com", "Уведомление")
except RuntimeError as e:
print(e)
# Превышен лимит: не более 3 вызовов за 10 сек. Повторите попытку позже.Этот пример демонстрирует, почему класс здесь уместнее функции: self.calls — это состояние, которое живёт между вызовами и сохраняется на весь период работы декоратора. С замыканием реализовать то же самое можно, но менее наглядно.
Рассмотрим ещё один пример из веб-разработки — фильтр для выборки данных. Такие объекты встречаются при реализации поиска и фильтрации в API: клиент передаёт параметры фильтрации, сервер создаёт объект-фильтр и применяет его к набору данных.
class QueryFilter:
"""
Фильтр для выборки записей по заданным критериям.
Хранит условия фильтрации и применяется к любому итерируемому набору данных.
"""
def __init__(self, **criteria):
# Критерии фильтрации сохраняются в объекте при создании.
# Например: QueryFilter(role="admin", is_active=True)
self.criteria = criteria
def __call__(self, record):
# При вызове проверяем, соответствует ли запись всем критериям.
# record — это словарь с данными одной записи.
for field, expected_value in self.criteria.items():
if record.get(field) != expected_value:
return False
return True
def __repr__(self):
criteria_str = ", ".join(f"{k}={v!r}" for k, v in self.criteria.items())
return f"QueryFilter({criteria_str})"
# Имитируем таблицу пользователей
users = [
{"id": 1, "name": "Alice", "role": "admin", "is_active": True},
{"id": 2, "name": "Bob", "role": "user", "is_active": True},
{"id": 3, "name": "Carol", "role": "moderator", "is_active": False},
{"id": 4, "name": "Dave", "role": "user", "is_active": False},
{"id": 5, "name": "Eve", "role": "admin", "is_active": True},
{"id": 6, "name": "Frank", "role": "user", "is_active": True},
]
# Создаём фильтры с разными критериями
active_admins = QueryFilter(role="admin", is_active=True)
inactive_users = QueryFilter(role="user", is_active=False)
print(active_admins) # QueryFilter(role='admin', is_active=True)
# Применяем фильтр через filter() — объект ведёт себя как функция-предикат
result = list(filter(active_admins, users))
for u in result:
print(u["name"], u["role"])
# Alice admin
# Eve admin
result = list(filter(inactive_users, users))
for u in result:
print(u["name"], u["role"])
# Dave userЭтот паттерн — «сконфигурированный объект, используемый как функция» — встречается в Django при написании кастомных менеджеров запросов и в DRF при создании фильтров для ViewSet.
Метод __call__ уместен в тех случаях, когда объект логически «делает одно главное действие», но при этом несёт в себе состояние или конфигурацию, необходимую для этого действия. Хороший признак того, что __call__ подходит: если вы обнаруживаете, что пишете замыкание и начинаете добавлять к нему вспомогательные функции для сброса состояния или доступа к внутренним значениям — класс с __call__ будет чище.
Типичные сценарии применения в веб-разработке: декораторы с состоянием (логирование, rate limiting, кеширование), объекты-предикаты для фильтрации данных, конфигурируемые обработчики событий, компоненты middleware.
Не стоит использовать __call__ там, где достаточно обычной функции или метода. Если объект не несёт состояния и не имеет конфигурации — лишняя обёртка в виде класса только усложнит код.
Метод __call__ позволяет объекту быть вызванным так же, как вызывается функция. При этом объект сохраняет все свои преимущества: хранит состояние между вызовами, допускает параметрическую конфигурацию при создании и может иметь дополнительные методы.
Наиболее важное практическое применение — декораторы на основе классов. Декоратор без параметров сохраняет оборачиваемую функцию в __init__ и выполняет её в __call__. Декоратор с параметрами сохраняет параметры в __init__, а в __call__ получает функцию и возвращает обёртку.
Функция callable() позволяет проверить, реализует ли объект __call__, не вызывая его.
- Чем принципиально отличается вызов
MyClass()от вызоваobj(), еслиobj— экземплярMyClass? - Что вернёт
callable(obj), если в классе объекта не определён метод__call__? - В декораторе на основе класса без параметров — в какой момент вызывается
__init__, а в какой__call__? - В декораторе с параметрами на основе класса — что именно возвращает
__call__? Почему? - В чём главное преимущество класса с
__call__перед замыканием, когда декоратору нужно хранить состояние? - Почему в декораторе без параметров
__call__принимает*args, **kwargs, а не конкретные аргументы? - В примере с
RateLimiterиз лекции__call__вызывается при декорировании и возвращаетwrapper. Где хранится список временных меток вызововself.calls? Будет ли он сбрасываться при каждом вызове задекорированной функции? - Можно ли передать объект с определённым
__call__в функциюmap()илиfilter()вместо обычной функции? Почему?
Класс Multiplier
Создайте класс Multiplier, объект которого при создании принимает один аргумент: factor (числовой коэффициент, int или float).
Реализуйте __call__, который принимает один аргумент x и возвращает x * factor.
Если x не является числом (int или float), выбрасывайте TypeError с сообщением "Аргумент должен быть числом".
Объект должен корректно работать с функцией map().
Пример использования:
double = Multiplier(2)
triple = Multiplier(3)
print(double(10)) # 20
print(triple(10)) # 30
numbers = [1, 2, 3, 4, 5]
print(list(map(double, numbers))) # [2, 4, 6, 8, 10]
try:
double("abc")
except TypeError as e:
print(e) # Аргумент должен быть числомКласс RenderList
Создайте класс RenderList, который при создании принимает один аргумент: list_type — строку "ul" или "ol".
Если передано любое другое значение, использовать "ul" по умолчанию.
Реализуйте __call__, который принимает список строк и возвращает корректную HTML-разметку: открывающий тег списка, каждый элемент в тегах <li>, закрывающий тег.
Метод должен возвращать строку, а не выводить её напрямую.
Пример использования:
render_ul = RenderList("ul")
render_ol = RenderList("ol")
render_bad = RenderList("div") # некорректный тип — используем "ul"
menu = ["Главная", "О нас", "Контакты"]
print(render_ul(menu))
# <ul>
# <li>Главная</li>
# <li>О нас</li>
# <li>Контакты</li>
# </ul>
print(render_ol(["Шаг 1", "Шаг 2", "Шаг 3"]))
# <ol>
# <li>Шаг 1</li>
# <li>Шаг 2</li>
# <li>Шаг 3</li>
# </ol>
print(render_bad.list_type) # ulКласс ImageFileAcceptor
Создайте класс ImageFileAcceptor, объект которого при создании принимает один аргумент: extensions — кортеж допустимых расширений файлов без точки, например ('jpg', 'jpeg', 'png').
Реализуйте __call__, который принимает имя файла в виде строки и возвращает True, если расширение файла входит в список допустимых, и False в противном случае.
Сравнение расширений должно быть нечувствительным к регистру. Используйте объект как предикат в функции filter().
Пример использования:
filenames = ["photo.jpg", "document.PDF", "avatar.PNG", "script.py", "logo.svg", "banner.JPEG"]
image_filter = ImageFileAcceptor(('jpg', 'jpeg', 'png'))
images = list(filter(image_filter, filenames))
print(images)
# ['photo.jpg', 'avatar.PNG', 'banner.JPEG']
print(image_filter("report.pdf")) # False
print(image_filter("cover.JPG")) # TrueКласс Logger (декоратор)
Реализуйте класс Logger, который работает как декоратор без параметров. При декорировании класс должен сохранять оборачиваемую функцию.
При каждом вызове задекорированной функции необходимо выводить строку вида Функция <имя> вызов <номер> — где номер увеличивается с каждым вызовом — выполнять оригинальную функцию и возвращать её результат.
Номер вызова должен быть доступен через атрибут call_count объекта-декоратора.
Пример использования:
@Logger
def add(a, b):
return a + b
@Logger
def greet(name):
return f"Привет, {name}!"
print(add(2, 3))
# Функция add вызов 1
# 5
print(add(10, 20))
# Функция add вызов 2
# 30
print(greet("Alice"))
# Функция greet вызов 1
# Привет, Alice!
print(add.call_count) # 2
print(greet.call_count) # 1Класс Cache (декоратор с кешированием)
Реализуйте класс Cache, который работает как декоратор и кеширует результаты вызовов функции.
Все результаты должны храниться в словаре _cache объекта-декоратора — ключами служат аргументы вызова, значениями — возвращённые результаты.
При повторном вызове с теми же аргументами функция не должна выполняться заново — результат берётся из кеша.
Если среди аргументов есть изменяемые типы данных (например, список или словарь), программа не должна завершаться с ошибкой — такие вызовы просто не кешируются и всегда выполняются заново.
При первом вычислении выводите строку Вычисляем..., при получении из кеша — Из кеша.
Пример использования:
@Cache
def add(a, b):
return a + b
print(add(2, 3)) # Вычисляем... / 5
print(add(2, 3)) # Из кеша / 5
print(add(10, 5)) # Вычисляем... / 15
print(add(10, 5)) # Из кеша / 15
print(add(2, 3)) # Из кеша / 5
# Вызов с изменяемым аргументом — кеш не используется, ошибки нет
print(add(2, [3, 4])) # Вычисляем... — список нельзя сохранить как ключ словаря