partial в классе Python потерял self: когда нужен partialmethod

Класс отправителя умеет менять состояние сообщения через общий метод set_state. Для удобства в нём появились mark_sent и mark_failed, созданные через functools.partial. При вызове sender.mark_sent() Python жалуется на отсутствующий аргумент: ожидаемая привязка self не произошла. Сам partial прекрасно работает с отдельными функциями, но атрибут класса участвует ещё в одном механизме.

Как появляется связанный метод

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

partialmethod специально предназначен для определения методов с заранее выбранными аргументами. При доступе через экземпляр он делегирует привязку исходному методу и добавляет собственные фиксированные значения. Поэтому в set_state попадёт правильный отправитель, а затем значение состояния. Это позволяет выразить несколько именованных операций через одно поведение, не дублируя короткие методы.

В примере изменяется состояние двух независимых экземпляров. Важно проверить не только удобное имя mark_sent, но и то, какой объект получил изменение. Если случайно связать метод с одним заранее созданным экземпляром, все вызовы могли бы менять его. Проверки второго сообщения показывают, что привязка происходит при каждом доступе через конкретный объект.

Зафиксировать аргумент метода

from functools import partialmethod

class Message:
    def __init__(self):
        self.state = "draft"
        self.history = []

    def set_state(self, state):
        self.state = state
        self.history.append(state)

    mark_sent = partialmethod(set_state, "sent")
    mark_failed = partialmethod(set_state, "failed")

first = Message()
second = Message()
first.mark_sent()
second.mark_failed()
assert first.state == "sent"
assert second.state == "failed"
assert first.history == ["sent"]
assert second.history == ["failed"]
assert first.history is not second.history
print(first.state, second.state)

Сохраните блок как обычный Python-файл. Результат sent failed вычисляется двумя вызовами методов. Общая реализация set_state по-прежнему отвечает за запись истории: новые именованные операции не должны обходить это правило. Если у каждого состояния появляется собственная сложная логика, явные методы могут стать понятнее набора частичных применений.

Выбрать границу привязки

Фиксированные изменяемые аргументы требуют осторожности. Список, переданный при определении класса, будет существовать как один объект определения, а не создаваться заново для каждого вызова. В примере фиксируются строки, а история создаётся внутри __init__. Такая граница позволяет повторно использовать описание операции и сохранять раздельное состояние сообщений.

Часто лучше связать именованный аргумент, если сигнатура исходного метода будет расширяться. Например, partialmethod(set_state, state="sent") яснее объясняет назначение значения, но вызывающий код может переопределить ключевой аргумент при вызове. Если операция обязана всегда означать именно sent, определите обычный метод без такого параметра и явно вызовите set_state. Удобство partialmethod не отменяет выбор публичного интерфейса.

Различайте создание callback для уже существующего экземпляра и объявление метода класса. В первом случае partial(message.set_state, "sent") получает уже связанный метод и работает естественно. Во втором экземпляр ещё неизвестен, поэтому нужен механизм будущей привязки. Проверяйте место создания объекта функции: этот вопрос часто сразу объясняет ошибку self.

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

Продолжить практику

Продолжите с методами, декораторами и functools в «Python для профессионалов». Откройте программу курса и разберите, как объект функции превращается в связанный метод: это помогает проектировать удобные интерфейсы классов и понимать поведение готовых обёрток.

Продолжить на практике

  • Python для профессионалов ↗

    Python для профессионалов — курс для людей, стремящихся углубить свои знания языка. Вы изучите итераторы, генераторы, декораторы, современные возможности Python (3.8+), освоите метапрограммирование, асинхронность, внутреннее устройство интерпретатора, инструменты оптимизации и профилирования, а также получите уверенность в использовании продвинутых возможностей стандартной библиотеки.