Передача параметра self при декорировании методов в Python

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

Это декоратор функций, который работает по назначению:

from functools import update_wrapper


class _PrintingArguments:
    def __init__(self, function, default_comment, comment_variable):
        self.function = function
        self.comment_variable = comment_variable
        self.default_comment = default_comment
        update_wrapper(wrapped=function, wrapper=self)

    def __call__(self, *args, **kwargs):
        comment = kwargs.pop(self.comment_variable, self.default_comment)
        params_str = [repr(arg) for arg in args] + ["{}={}".format(k, repr(v)) for k, v in kwargs.items()]
        function_call_log = "{}({})".format(self.function.__name__, ", ".join(params_str))
        print("Function execution - '{}'\n\t{}".format(comment, function_call_log))
        function_return = self.function(*args, **kwargs)
        print("\tFunction executed\n")
        return function_return


def function_log(_function=None, default_comment="No comment.", comment_variable="comment"):
    if _function is None:
        def decorator(func):
            return _PrintingArguments(function=func, default_comment=default_comment, comment_variable=comment_variable)
        return decorator
    else:
        return _PrintingArguments(function=_function, default_comment=default_comment, comment_variable=comment_variable)

# example use:
@function_log
def a(*args, **kwargs):
    pass


@function_log(default_comment="Hello World!", comment_variable="comment2")
def b(*args, **kwargs):
    pass


a(0, x=1, y=2)
a(0, x=1, y=2, comment="Custom comment!")

b("a", "b", "c", asd="something")
b("a", "b", "c", asd="something", comment2="Custom comment for b!")

Результат выполнения кода:

Function execution - 'No comment.'
    a(0, y=2, x=1)
    Function executed

Function execution - 'Custom comment!'
    a(0, y=2, x=1)
    Function executed

Function execution - 'Hello World!'
    b('a', 'b', 'c', asd='something')
    Function executed

Function execution - 'Custom comment for b!'
    b('a', 'b', 'c', asd='something')
    Function executed



Я пробовал точно такой же декоратор для методов:

class A:
    def __init__(self):
        pass

    @function_log
    def method1(self, *args, **kwargs):
        print("\tself = {}".format(self))

    @function_log(default_comment="Something", comment_variable="comment2")
    def method2(self, *args, **kwargs):
        print("\tself = {}".format(self))

a_obj = A()

a_obj.method1(0, 1, p1="abc", p2="xyz")
a_obj.method1(0, 1, p1="abc", p2="xyz", comment="My comment")

a_obj.method2("a", "b", p1="abc", p2="xyz")
a_obj.method2("a", "b", p1="abc", p2="xyz", comment="My comment 2")

Результат:

Function execution - 'No comment.'
    method1(0, 1, p2='xyz', p1='abc')
    self = 0
    Function executed

Function execution - 'My comment'
    method1(0, 1, p2='xyz', p1='abc')
    self = 0
    Function executed

Function execution - 'Something'
    method2('a', 'b', p2='xyz', p1='abc')
    self = a
    Function executed

Function execution - 'Something'
    method2('a', 'b', comment='My comment 2', p2='xyz', p1='abc')
    self = a
    Function executed

Мой декоратор не передает в этот параметр параметр self.
Я хочу написать второй декоратор method_log, который будет работать примерно так же, как function_log. Для кода:

class A:
    def __init__(self):
        pass

    @method_log
    def method1(self, *args, **kwargs):
        print("\tself = {}".format(self))

    @fmethod_log(default_comment="Something", comment_variable="comment2")
    def method2(self, *args, **kwargs):
        print("\tself = {}".format(self))

a_obj = A()

a_obj.method1(0, 1, p1="abc", p2="xyz")
a_obj.method1(0, 1, p1="abc", p2="xyz", comment="My comment")

a_obj.method2("a", "b", p1="abc", p2="xyz")
a_obj.method2("a", "b", p1="abc", p2="xyz", comment="My comment 2")

Мне нужен результат:

Method execution - 'No comment.'
    method1(<__main__.A instance at ...>, 0, 1, p2='xyz', p1='abc')
    self = <__main__.A instance at ...> #
    Function executed

Method execution - 'My comment'
    method1(<__main__.A instance at ...>, 0, 1, p2='xyz', p1='abc')
    self = <__main__.A instance at ...>
    Function executed

Method execution - 'Something'
    method2(<__main__.A instance at ...>, 'a', 'b', p2='xyz', p1='abc')
    self = <__main__.A instance at ...>
    Function executed

Method execution - 'Something'
    method2(<__main__.A instance at ...>, 'a', 'b', comment='My comment 2', p2='xyz', p1='abc')
    self = <__main__.A instance at ...>
    Function executed

2 ответа

Решение

Это не работает с вашим текущим дизайном из-за того, как классы работают в Python.

Когда создается экземпляр класса, функции в нем привязываются к экземпляру - они становятся связанными методами, так что self автоматически передается.

Вы можете увидеть это:

class A:
    def method1(self):
        pass

>>> A.method1
<function A.method1 at 0x7f303298ef28>
>>> a_instance = A()
>>> a_instance.method1
<bound method A.method1 of <__main__.A object at 0x7f303a36c518>>

Когда создается экземпляр A, method1 волшебным образом превращается изfunction в bound method.

Ваш декоратор заменяет method1 - вместо реальной функции теперь это экземпляр _PrintingArguments. Магия, которая превращает функции в связанные методы, не применяется к случайным объектам, даже если они определяют__call__чтобы они вели себя как функция. (Но эту магию можно применить, если ваш класс реализует протокол дескриптора, см. Ответ ShadowRanger!).

class Decorator:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        return self.func(*args, **kwargs)


class A:
    @Decorator
    def method1(self):
        pass

>>> A.method1
<__main__.Decorator object at 0x7f303a36cbe0>
>>> a_instance = A()
>>> a_instance.method1
<__main__.Decorator object at 0x7f303a36cbe0>

Нет никакой магии. method1 в экземпляре A не является связанным методом, это просто случайный объект с __call__ метод, который не будет иметьself прошел автоматически.

Если вы хотите украсить методы, вы должны заменить декорированную функцию другой реальной функцией, произвольным объектом с __call__ не пойдет.

Вы можете адаптировать свой текущий код для возврата реальной функции:

import functools

class _PrintingArguments:
    def __init__(self, default_comment, comment_variable):
        self.comment_variable = comment_variable
        self.default_comment = default_comment

    def __call__(self, function):
        @functools.wraps(function)
        def decorated(*args, **kwargs):
            comment = kwargs.pop(self.comment_variable, self.default_comment)
            params_str = [repr(arg) for arg in args] + ["{}={}".format(k, repr(v)) for k, v in kwargs.items()]
            function_call_log = "{}({})".format(function.__name__, ", ".join(params_str))
            print("Function execution - '{}'\n\t{}".format(comment, function_call_log))
            function_return = function(*args, **kwargs)
            print("\tFunction executed\n")
            return function_return
        return decorated

def function_log(_function=None, default_comment="No comment.", comment_variable="comment"):
    decorator = _PrintingArguments(
        default_comment=default_comment,
        comment_variable=comment_variable,
    )
    if _function is None:
        return decorator
    else:
        return decorator(_function)

Если ты хочешь _PrintingArgumentsчтобы связать так же, как обычную функцию, это действительно возможно, вам просто нужно самостоятельно реализовать протокол дескриптора, чтобы он соответствовал поведению встроенных функций. Удобно, что Python предоставляетtypes.MethodType, который может быть использован для создания связанного метода из любого вызываемого объекта, для которого требуется привязка, поэтому мы используем его для реализации нашего дескриптора__get__:

import types

class _PrintingArguments:
    # __init__ and __call__ unchanged

    def __get__(self, instance, owner):
        if instance is None:
            return self  # Accessed from class, return unchanged
        return types.MethodType(self, instance)  # Accessed from instance, bind to instance

Это работает так, как вы ожидаете, на Python 3 (попробуйте онлайн!). На Python 2 это еще проще (поскольку существуют несвязанные методы, поэтому вызовtypes.MethodType можно сделать безоговорочно):

import types

class _PrintingArguments(object):  # Explicit inheritance from object needed for new-style class on Py2
    # __init__ and __call__ unchanged

    def __get__(self, instance, owner):
        return types.MethodType(self, instance, owner)  # Also pass owner

Попробуйте онлайн!

Для немного лучшей производительности (только на Python 2) вы можете вместо этого сделать:

class _PrintingArguments(object):  # Explicit inheritance from object needed for new-style class on Py2
    # __init__ and __call__ unchanged

# Defined outside class, immediately after dedent
_PrintingArguments.__get__ = types.MethodType(types.MethodType, None, _PrintingArguments)

что продвигает реализацию __get__ на слой C, создав несвязанный метод из types.MethodType сам, удаляя накладные расходы интерпретатора байтового кода из каждого вызова.

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