Я пишу декоратор регистратора, который можно применять, среди прочего, к методам класса (и теоретически к любым функциям или методам). Моя проблема в том, что их параметризация меняется в зависимости от того, является ли функция
Пример кода, который работает, но очень элегантен и его можно обмануть с помощью параметризации:
def some_decorator(func):
def wrapper(*argsWrap, **kwargsWrap):
if isinstance(func, classmethod):
if isinstance(argsWrap[0], SomeClass):
# Called by some_instance.some_method as the first parameter is SomeClass
print(argsWrap[1])
return func.__func__(*argsWrap, **kwargsWrap)
else:
# Called by SomeClass.some_method
print(argsWrap[0])
return func.__func__(SomeClass, *argsWrap, **kwargsWrap)
else:
return func(*argsWrap, **kwargsWrap)
return wrapper
class SomeClass:
@some_decorator
@classmethod
def some_method(cls, some_var: str):
print(some_var)
if __name__ == '__main__':
SomeClass.some_method('Test')
some_instance = SomeClass()
some_instance.some_method('Test2')
Вопрос простой: можно ли принять более чистое и безопасное решение, касающееся только параметризации?
Обратите внимание, что для декоратора общего назначения (используемого другими) нельзя сказать, что его нужно размещать ниже метода @class, а не выше и т. д.
Также обратите внимание, что это решение является «плохим» обходным решением в том смысле, что можно удобно представить функции, которые используют больше экземпляров одного и того же класса (например, типичный перегруженный оператор, например __eq__
, работает следующим образом).
Также обратите внимание, что реальная проблема, как я сузил ее, заключается в том, что метод класса, вызываемый классом или вызываемый экземпляром класса, передает различную параметризацию (первый параметр является первым аргументом для функции, привязанной к классу, и ссылкой на класс для функция, привязанная к экземпляру).
fОжидаемая функциональность состоит в том, чтобы написать функцию «пользователем», пусть это будет метод @class или что-то еще, И ЗАТЕМ, если необходимо, поместить мой декоратор для ведения журнала. Поэтому из-за ожидаемого рабочего процесса очень разумно, чтобы мой декоратор был внешним/верхним, регистрирующим все, что находится внутри (пусть это будет любое количество/тип декораторов, включая метод @class).
Для этого я использую декоратор классов __call__
и __get__
.
Вот:
class LoggerDecorator:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs)
def __get__(self, instance, owner):
if not instance:
return self.func.__get__(owner, owner)
return self.func.__get__(instance, owner)
Ключевая идея этой реализации — использовать методы __get__
и __call__
в декораторе класса для обработки различных типов вызовов функций.
Проблема, с которой мы здесь сталкиваемся, заключается в том, что декоратор используется как универсальный способ обертывания различных функций, но получаемые им параметры различаются в зависимости от типа вызываемой функции (например, classmethod(cls, )
, staticmethod()
, instance method(self, )
). Как отметил автор темы, параметризация (например, использование типа первого параметра функции) в качестве определяющего условия для функции-обертки, несомненно, рискованна.
Здесь вместо декоратора функции мы используем декоратор класса. Условие обработки декоратора перенесено с «параметризации» вспомогательной функции-оболочки на дескриптор __get__
, который определяет, используется ли функция экземпляром, созданным экземпляром.
__get__
: метод __get__
вызывается для получения атрибута класса-владельца (доступ к атрибуту класса) или экземпляра этого класса (доступ к атрибуту экземпляра).
Давайте возьмем «метод класса» в качестве примера и сравним различия в методе get, когда он вызывается «напрямую» (обычное использование) и когда он вызывается созданным экземпляром:
class LoggerDecorator:
def __init__(self, func):
self.func = func
def __get__(self, instance, owner):
print(f'self: {self}')
print(f'instance: {instance}')
print(f'owner: {owner}')
return self.func.__get__(owner, owner)
class SomeClass:
@LoggerDecorator
@classmethod
def class_method(cls, some_var):
print(f"print some_var: {some_var}")
# Call Directly
SomeClass.class_method("Call Directly")
# Called by instance.
s = SomeClass()
s.class_method("Called by instance")
# OUTPUT
--- Called by directly ---
self: <__main__.LoggerDecorator object at 0x7f36aad05590>
instance: None
owner: <class '__main__.SomeClass'>
print some_var: Call Directly
--- Called by instance ---
self: <__main__.LoggerDecorator object at 0x7f36aad05590>
instance: <__main__.SomeClass object at 0x7f36aad06150>
owner: <class '__main__.SomeClass'>
print some_var: Called by instance
Мы можем заметить, что при прямом вызове экземпляром будет None
. И результат тот же для статического метода (для его вызова необходим экземпляр). Следовательно, мы можем использовать, является ли экземпляр в __get__
None
в качестве определяющего фактора.
__call__
: Метод __call__
отвечает за оформление «автономных функций» или любой функции, не принадлежащей классу. Когда мы декорируем функцию следующим образом:
@LoggerDecorator
def standalone_function():
return
что равно
LoggerDecorator(standalone_function)
и это запускает метод __call__
.
class LoggerDecorator:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs)
def __get__(self, instance, owner):
print(f'self: {self}')
print(f'instance: {instance}')
print(f'owner: {owner}')
if not instance:
return self.func.__get__(owner, owner)
return self.func.__get__(instance, owner)
class SomeClass:
@LoggerDecorator
@classmethod
def class_method(cls, some_var: str, *args, **kwargs):
print(f"Class method: {some_var}")
print(f"Class method args: {args}")
print(f"Class method kwargs: {kwargs}")
@LoggerDecorator
def instance_method(self, some_var: str, *args, **kwargs):
print(f"Instance method: {some_var}")
print(f"Instance method args: {args}")
print(f"Instance method kwargs: {kwargs}")
@LoggerDecorator
@staticmethod
def static_method(some_var: str, *args, **kwargs):
print(f"Static method: {some_var}")
print(f"Static method args: {args}")
print(f"Static method kwargs: {kwargs}")
@LoggerDecorator
def standalone_function(some_var: str, *args, **kwargs):
print(f"Standalone function: {some_var}")
print(f"Standalone function args: {args}")
print(f"Standalone function kwargs: {kwargs}")
if __name__ == '__main__':
### Class Method ###
# Call Directly
print("### Class Method ###")
print("--- Called by directly ---")
SomeClass.class_method("Test1", "class_directly", a1 = "class_a", b1 = "class_b")
# Called by instance.
print("")
print("--- Called by instance ---")
s = SomeClass()
s.class_method("Test2", "class_by_instance", a1 = "class_a", b1 = "class_b")
### Static Method ###
# Call Directly
print("")
print("### Static Method ###")
print("--- Called by directly ---")
SomeClass.static_method("Test3", "static_directly", a1 = "static_a", b1 = "static_b")
# Called by instance.
print("")
print("--- Called by instance ---")
s.static_method("Test4", "static_by_instance", a1 = "static_a", b1 = "static_b")
### Instance Method ###
# Call only by instance
print("")
print("### Instance Method ###")
s.instance_method("Test5", "instance", a1 = "instance_a", b1 = "instance_b")
### Standalone Function
print("")
print("### Standalone Function ###")
standalone_function("Test6", "standalone", a1 = "standalone_a", b1 = "standalone_b")
### Class Method ###
--- Called by directly ---
self: <__main__.LoggerDecorator object at 0x7f858c34e390>
instance: None
owner: <class '__main__.SomeClass'>
Class method: Test1
Class method args: ('class_directly',)
Class method kwargs: {'a1': 'class_a', 'b1': 'class_b'}
--- Called by instance ---
self: <__main__.LoggerDecorator object at 0x7f858c34e390>
instance: <__main__.SomeClass object at 0x7f858c34f410>
owner: <class '__main__.SomeClass'>
Class method: Test2
Class method args: ('class_by_instance',)
Class method kwargs: {'a1': 'class_a', 'b1': 'class_b'}
### Static Method ###
--- Called by directly ---
self: <__main__.LoggerDecorator object at 0x7f858c34f0d0>
instance: None
owner: <class '__main__.SomeClass'>
Static method: Test3
Static method args: ('static_directly',)
Static method kwargs: {'a1': 'static_a', 'b1': 'static_b'}
--- Called by instance ---
self: <__main__.LoggerDecorator object at 0x7f858c34f0d0>
instance: <__main__.SomeClass object at 0x7f858c34f410>
owner: <class '__main__.SomeClass'>
Static method: Test4
Static method args: ('static_by_instance',)
Static method kwargs: {'a1': 'static_a', 'b1': 'static_b'}
### Instance Method ###
self: <__main__.LoggerDecorator object at 0x7f858c34e510>
instance: <__main__.SomeClass object at 0x7f858c34f410>
owner: <class '__main__.SomeClass'>
Instance method: Test5
Instance method args: ('instance',)
Instance method kwargs: {'a1': 'instance_a', 'b1': 'instance_b'}
### Standalone Function ###
Standalone function: Test6
Standalone function args: ('standalone',)
Standalone function kwargs: {'a1': 'standalone_a', 'b1': 'standalone_b'}
Хороший подход, но, ИМХО, кажется, все еще глючный. В первой строке «SomeClass.class_method» аргумент «cls» получает первый аргумент «Test1» вместо «some_variable». Вместо этого 'cls' должен получить ссылку на класс.
Я думаю, что это все еще не работает и, похоже, не улавливает разницу между методом класса, вызываемым в классе, и вызываемым в экземпляре. Проверьте это на наличие SomeClass.class_method("Test1") и some_instance.class_method("Test2"). Теоретически оба должны работать и давать один и тот же результат.
Это работает для вас сейчас? Я немного изменил выражение вывода журнала, но потом понял, что сделал что-то лишнее. ржу не могу
К сожалению, нет. Этот подход по-прежнему не может провести различие между class_method, вызываемым в классе, и вызываемым в экземпляре. Я взял на себя смелость изменить приведенный выше пример кода, чтобы показать вам проблему; Вы можете изменить его обратно. Спасибо.
Ой! Моя вина. Спасибо за ваше терпение. Я наконец понял вашу точку зрения. Вы хотите, чтобы этот декоратор работал со всеми вызовами функций. Определение «работы» означает: 1. Декоратор должен правильно обрабатывать параметры для всех типов функций (метод класса, статический метод, метод экземпляра, отдельная функция и даже метод класса, вызываемый экземпляром). 2. Декоратор должен иметь возможность напрямую декорировать функции без необходимости изменения порядка декораторов. Верно ли мое понимание?
Я думаю, что использование декоратора классов с __call__
и __get__
могло бы помочь.
Я думаю, что вы находитесь на правильном подходе, и это приближается - проблема с этим кодом в том, что __get__
возвращает декорированный вызываемый объект напрямую - поэтому __call__
в вашем LoggerDecorator
классе никогда не вызывается (и никогда ничего не регистрирует). Скорее, он должен принять во внимание параметры owner
и instance
и вернуть self
, а __call__
должен быть тем, кто извлекает метод из класса. Это нужно делать осторожно, чтобы функции (в отличие от методов) также работали (в данном случае __call__
вызывается напрямую).
Ух ты, вроде пока работает, хотя я так и не понял, как __get__
называется и кем. Большое спасибо.
С другой стороны, в моем оригинальном декораторе я использовал `__call__` для всего, теперь мне нужно выяснить, как __get__
вернет управление __call__
, если это возможно.
Хм, я сделал еще одну небольшую модификацию, я установил self.func для полученной функции и возвращаю self.__call__
, таким образом я всегда прохожу self.__call__
(таким образом регистрируясь в одной функции), пока все работает - пока. Огромное спасибо за Вашу работу, отличная учеба.
Ой! Я нашел еще один вопрос, лол. Вроде не надо проверять if not instance
, все равно работает.
на самом деле, если он предназначен для работы как с простыми функциями, методами, так и с методами класса, вполне разумно потребовать, чтобы в сочетании с методами класса он помещался либо «выше», либо «ниже» @classmethod, а не «в любом случае». ". Просто задокументируйте это правильно. Но да, это можно заставить работать так, как вы планируете,