Моя конечная цель — написать систему, позволяющую легко записывать вызовы функций (в частности, методов класса).
Я начал с написания класса 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)
Я предполагаю, что использование декоратора вместо оболочки вызвало эту ошибку, но я не могу понять, почему и что мне следует сделать, чтобы ее исправить.
Кто-нибудь знает, как таким образом правильно набрать декоратор?
Также обратите внимание на defaultdict, чтобы упростить wrapper тело, а также на Self от typing, чтобы вообще избежать CountType. Кроме того, признаком того, что ваш класс не должен быть универсальным, является тот факт, что универсальный параметр нигде не используется в __init__.
@MarioIshac, можешь показать дорогу с помощью Self? Вы не можете использовать Self в статических методах, а в методе класса это будет неправильно (всегда разрешается к FunctionCount, а не к классу, в котором вы вызываете .count).
(а еще есть метод dict.setdefault для таких простых случаев, если вы не хотите таким образом менять публичный интерфейс)
@STerliakov mypy-play.net/…, однако mypy вынуждает использовать A.count из-за упомянутой вами проблемы. Однако дизайн OP выглядит хрупким, поскольку создание двух экземпляров FunctionCount перезапишет count_dict. Поскольку состояние в любом случае инициализируется вне класса, я считаю, что полное удаление класса — лучший подход.
Неважно, я только что понял, что mypy на самом деле имеет ошибку при принятии приведенного выше кода. Он не работает в pyright и во время выполнения Python из-за ссылки A внутри A. Извините, вы правы, Self здесь нельзя использовать.






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__. Это потому, что его предполагается применять к методам?
Я только что написал желаемую подпись. Функция __call__ всегда имеет self в качестве первого параметра, а также методы, которые вы хотите украсить. Так что вам это нужно как дополнительный аргумент. Но я это не проверял. Работает ли это для вас?
Я думаю, что это действительно должен быть ответ, а не комментарий.
Ваш класс не должен быть универсальным в 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 не был позиционным, поэтому я это исправил.
Ваш класс не должен быть универсальным в CountType — метод
countявляется универсальным по своей конструкции, но не сам класс. Кроме того, я настоятельно рекомендую запуститьmypy --strict— это указывало бы на другие проблемы в вашем коде, вызывающие выводNever. mypy-play.net/…