Подсказки типов Python для декораторов классов со ссылками на себя

Моя конечная цель — написать систему, позволяющую легко записывать вызовы функций (в частности, методов класса).

Я начал с написания класса Loggable с методом-оберткой, который позволяет мне украшать методы подклассов и записывать их вызовы.

Param = ParamSpec("Param")
RetType = TypeVar("RetType")


CountType = TypeVar("CountType", bound = "FunctionCount")


class FunctionCount(Generic[CountType]):
    def __init__(self, count_dict: dict[str, int]) -> None:
        self.count_dict = count_dict

    @staticmethod
    def count(
        func: Callable[Concatenate[CountType, Param], RetType],
    ) -> Callable[Concatenate[CountType, Param], RetType]:
        def wrapper(
            self: CountType, *args: Param.args, **kwargs: Param.kwargs
        ) -> RetType:
            function_name = f"{self.__class__.__name__}.{func.__name__}"
            if function_name not in self.count_dict:
                self.count_dict[function_name] = 0
            self.count_dict[function_name] += 1

            return func(self, *args, **kwargs)

        return wrapper

Теперь я могу писать подклассы и записывать их вызовы:

class A(FunctionCount):
    def __init__(self, count_dict: dict[str, int]) -> None:
        super().__init__(count_dict)

    @FunctionCount.count
    def func(self) -> None:
        pass

    @FunctionCount.count
    def func2(self) -> None:
        pass


count_dict: dict[str, int] = {}

a = A(count_dict)

a.func()
a.func()
a.func2()

print(count_dict)
assert count_dict == {"A.func": 2, "A.func2": 1}

Это работает очень хорошо, и я был рад. Но потом я подумал, что было бы неплохо иметь собственные имена для методов, поэтому заменил обертку на декоратор.

class FunctionCount(Generic[CountType]):
    def __init__(self, count_dict: dict[str, int]) -> None:
        self.count_dict = count_dict

    @staticmethod
    def count(
        f_name: str | None = None,
    ) -> Callable[
        [Callable[Concatenate[CountType, Param], RetType]],
        Callable[Concatenate[CountType, Param], RetType],
    ]:
        def decorator(
            func: Callable[Concatenate[CountType, Param], RetType],
        ) -> Callable[Concatenate[CountType, Param], RetType]:
            def wrapper(
                self: CountType, *args: Param.args, **kwargs: Param.kwargs
            ) -> RetType:
                function_name = f_name or f"{self.__class__.__name__}.{func.__name__}"
                if function_name not in self.count_dict:
                    self.count_dict[function_name] = 0
                self.count_dict[function_name] += 1

                return func(self, *args, **kwargs)

            return wrapper

        return decorator

Тогда мне просто пришлось изменить вызовы декоратора

class A(FunctionCount):
    def __init__(self, count_dict: dict[str, int]) -> None:
        super().__init__(count_dict)

    @FunctionCount.count()
    def func(self) -> None:
        pass

    @FunctionCount.count("custom_name")
    def func2(self) -> None:
        pass

a.func()
a.func()
a.func2()

print(count_dict)
assert count_dict == {"A.func": 2, "custom_name": 1}

Эти скрипты тоже работают очень хорошо, но теперь у mypy у меня тяжелые времена. Когда я вызываю метод a.func, я получаю следующую ошибку mypy:

Неверный аргумент self "A" для атрибута функции "func" с типом "Callable[[Never], None]" mypy(misc)

Я предполагаю, что использование декоратора вместо оболочки вызвало эту ошибку, но я не могу понять, почему и что мне следует сделать, чтобы ее исправить.

Кто-нибудь знает, как таким образом правильно набрать декоратор?

Ваш класс не должен быть универсальным в CountType — метод count является универсальным по своей конструкции, но не сам класс. Кроме того, я настоятельно рекомендую запустить mypy --strict — это указывало бы на другие проблемы в вашем коде, вызывающие вывод Never. mypy-play.net/…

STerliakov 30.07.2024 18:53

Также обратите внимание на defaultdict, чтобы упростить wrapper тело, а также на Self от typing, чтобы вообще избежать CountType. Кроме того, признаком того, что ваш класс не должен быть универсальным, является тот факт, что универсальный параметр нигде не используется в __init__.

Mario Ishac 30.07.2024 19:01

@MarioIshac, можешь показать дорогу с помощью Self? Вы не можете использовать Self в статических методах, а в методе класса это будет неправильно (всегда разрешается к FunctionCount, а не к классу, в котором вы вызываете .count).

STerliakov 30.07.2024 19:10

(а еще есть метод dict.setdefault для таких простых случаев, если вы не хотите таким образом менять публичный интерфейс)

STerliakov 30.07.2024 19:13

@STerliakov mypy-play.net/…, однако mypy вынуждает использовать A.count из-за упомянутой вами проблемы. Однако дизайн OP выглядит хрупким, поскольку создание двух экземпляров FunctionCount перезапишет count_dict. Поскольку состояние в любом случае инициализируется вне класса, я считаю, что полное удаление класса — лучший подход.

Mario Ishac 30.07.2024 19:28

Неважно, я только что понял, что mypy на самом деле имеет ошибку при принятии приведенного выше кода. Он не работает в pyright и во время выполнения Python из-за ссылки A внутри A. Извините, вы правы, Self здесь нельзя использовать.

Mario Ishac 30.07.2024 19:35
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
0
6
66
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

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

class Decorator(Protocol, Generic[CountType, Param, RetType]):
    __call__(self, __self__: CountType, *args: Param.args, **kwargs: Param.kwargs) -> RetType:
        ...


class FunctionCount(Generic[CountType]):
    def __init__(self, count_dict: dict[str, int]) -> None:
        self.count_dict = count_dict

    @staticmethod
    def count(
        f_name: str | None = None,
    ) -> Decorator[CountType, Param, RetType]:
        def decorator(
            func: Callable[Concatenate[CountType, Param], RetType],
        ) -> Callable[Concatenate[CountType, Param], RetType]:
            def wrapper(
                self: CountType, *args: Param.args, **kwargs: Param.kwargs
            ) -> RetType:
                function_name = f_name or f"{self.__class__.__name__}.{func.__name__}"
                if function_name not in self.count_dict:
                    self.count_dict[function_name] = 0
                self.count_dict[function_name] += 1

                return func(self, *args, **kwargs)

            return wrapper

        return decorator

Я не понимаю, почему параметр __self__. Это потому, что его предполагается применять к методам?

Paulo-99 31.07.2024 09:27

Я только что написал желаемую подпись. Функция __call__ всегда имеет self в качестве первого параметра, а также методы, которые вы хотите украсить. Так что вам это нужно как дополнительный аргумент. Но я это не проверял. Работает ли это для вас?

lord_haffi 31.07.2024 14:16
Ответ принят как подходящий

Я думаю, что это действительно должен быть ответ, а не комментарий.

Ваш класс не должен быть универсальным в CountType — метод count является универсальным по своей конструкции, но не сам класс. Вы пытаетесь написать «статический метод, который работает с некоторым вызываемым объектом, первый аргумент которого является подтипом FunctionCount», а не «класс, который может применять свой статический метод только к вызываемому объекту с помощью...», верно? Тогда универсальным должен быть сам статический метод, а не класс! Сравните со следующим:

from typing import ParamSpec, Callable, TypeVar, Concatenate

Param = ParamSpec("Param")
RetType = TypeVar("RetType")
CountType = TypeVar("CountType", bound = "FunctionCount")

class FunctionCount:
    def __init__(self, count_dict: dict[str, int]) -> None:
        self.count_dict = count_dict

    @staticmethod
    def count(
        f_name: str | None = None,
    ) -> Callable[
        [Callable[Concatenate[CountType, Param], RetType]],
        Callable[Concatenate[CountType, Param], RetType],
    ]:
        def decorator(
            func: Callable[Concatenate[CountType, Param], RetType],
        ) -> Callable[Concatenate[CountType, Param], RetType]:
            def wrapper(
                self: CountType, /, *args: Param.args, **kwargs: Param.kwargs
            ) -> RetType:
                function_name = f_name or f"{self.__class__.__name__}.{func.__name__}"
                if function_name not in self.count_dict:
                    self.count_dict[function_name] = 0
                self.count_dict[function_name] += 1

                return func(self, *args, **kwargs)

            return wrapper
            
        return decorator

Это проходит mypy --strict (площадка mypy , площадка пирайт).

Кроме того, запускайте mypy --strict, когда вы сталкиваетесь с некоторыми mypy ошибками, которые содержат Never (или просто что-то «странное», что вам трудно понять) — это может указывать на другие места, где вам что-то mypy не нравится. mypy --strict выдает кучу ошибок в исходном коде, одна из самых важных — отсутствие общего аргумента для подкласса (см. здесь).

Спасибо, это сработало! Я видел эту Generic механику в другом посте stackoverflow, но предполагаю, что в моем случае она неприменима! У меня также была еще одна ошибка из-за того, что self не был позиционным, поэтому я это исправил.

Paulo-99 31.07.2024 09:25

Другие вопросы по теме