Как определить, вызывается ли метод класса в экземпляре или в классе

Я пишу декоратор регистратора, который можно применять, среди прочего, к методам класса (и теоретически к любым функциям или методам). Моя проблема в том, что их параметризация меняется в зависимости от того, является ли функция

  • автономная_функция(*args)
  • методmember_method(self, *args) или
  • class_method(cls, *args)

Пример кода, который работает, но очень элегантен и его можно обмануть с помощью параметризации:

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__, работает следующим образом).

Также обратите внимание, что реальная проблема, как я сузил ее, заключается в том, что метод класса, вызываемый классом или вызываемый экземпляром класса, передает различную параметризацию (первый параметр является первым аргументом для функции, привязанной к классу, и ссылкой на класс для функция, привязанная к экземпляру).

на самом деле, если он предназначен для работы как с простыми функциями, методами, так и с методами класса, вполне разумно потребовать, чтобы в сочетании с методами класса он помещался либо «выше», либо «ниже» @classmethod, а не «в любом случае». ". Просто задокументируйте это правильно. Но да, это можно заставить работать так, как вы планируете,

jsbueno 31.08.2024 19:22

fОжидаемая функциональность состоит в том, чтобы написать функцию «пользователем», пусть это будет метод @class или что-то еще, И ЗАТЕМ, если необходимо, поместить мой декоратор для ведения журнала. Поэтому из-за ожидаемого рабочего процесса очень разумно, чтобы мой декоратор был внешним/верхним, регистрирующим все, что находится внутри (пусть это будет любое количество/тип декораторов, включая метод @class).

Gyula Sámuel Karli 31.08.2024 20:24
Почему в 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 может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
2
96
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Для этого я использую декоратор классов __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__, который определяет, используется ли функция экземпляром, созданным экземпляром.

  1. __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 в качестве определяющего фактора.

  2. __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' должен получить ссылку на класс.

Gyula Sámuel Karli 30.08.2024 14:27

Я думаю, что это все еще не работает и, похоже, не улавливает разницу между методом класса, вызываемым в классе, и вызываемым в экземпляре. Проверьте это на наличие SomeClass.class_method("Test1") и some_instance.class_method("Test2"). Теоретически оба должны работать и давать один и тот же результат.

Gyula Sámuel Karli 30.08.2024 17:35

Это работает для вас сейчас? Я немного изменил выражение вывода журнала, но потом понял, что сделал что-то лишнее. ржу не могу

Yu Wei Liu 30.08.2024 18:45

К сожалению, нет. Этот подход по-прежнему не может провести различие между class_method, вызываемым в классе, и вызываемым в экземпляре. Я взял на себя смелость изменить приведенный выше пример кода, чтобы показать вам проблему; Вы можете изменить его обратно. Спасибо.

Gyula Sámuel Karli 31.08.2024 16:52

Ой! Моя вина. Спасибо за ваше терпение. Я наконец понял вашу точку зрения. Вы хотите, чтобы этот декоратор работал со всеми вызовами функций. Определение «работы» означает: 1. Декоратор должен правильно обрабатывать параметры для всех типов функций (метод класса, статический метод, метод экземпляра, отдельная функция и даже метод класса, вызываемый экземпляром). 2. Декоратор должен иметь возможность напрямую декорировать функции без необходимости изменения порядка декораторов. Верно ли мое понимание?

Yu Wei Liu 31.08.2024 18:47

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

Yu Wei Liu 31.08.2024 18:47

Я думаю, что вы находитесь на правильном подходе, и это приближается - проблема с этим кодом в том, что __get__ возвращает декорированный вызываемый объект напрямую - поэтому __call__ в вашем LoggerDecorator классе никогда не вызывается (и никогда ничего не регистрирует). Скорее, он должен принять во внимание параметры owner и instance и вернуть self, а __call__ должен быть тем, кто извлекает метод из класса. Это нужно делать осторожно, чтобы функции (в отличие от методов) также работали (в данном случае __call__ вызывается напрямую).

jsbueno 31.08.2024 19:19

Ух ты, вроде пока работает, хотя я так и не понял, как __get__ называется и кем. Большое спасибо.

Gyula Sámuel Karli 31.08.2024 23:19

С другой стороны, в моем оригинальном декораторе я использовал `__call__` для всего, теперь мне нужно выяснить, как __get__ вернет управление __call__, если это возможно.

Gyula Sámuel Karli 31.08.2024 23:29

Хм, я сделал еще одну небольшую модификацию, я установил self.func для полученной функции и возвращаю self.__call__, таким образом я всегда прохожу self.__call__ (таким образом регистрируясь в одной функции), пока все работает - пока. Огромное спасибо за Вашу работу, отличная учеба.

Gyula Sámuel Karli 31.08.2024 23:46

Ой! Я нашел еще один вопрос, лол. Вроде не надо проверять if not instance, все равно работает.

Yu Wei Liu 31.08.2024 23:47

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