Метакласс, который украшает все методы класса, используя две разные реализации декоратора.

У меня проблема с реализацией декоратора, применяемого в этом декораторе метакласса, который я написал:

def decorateAll(decorator):
    class MetaClassDecorator(type):

        def __new__(meta, classname, supers, classdict):
            for name, elem in classdict.items():
                if type(elem) is FunctionType:
                    classdict[name] = decorator(classdict[name])
            return type.__new__(meta, classname, supers, classdict)
    return MetaClassDecorator

Это класс, в котором я использовал метакласс:

class Account(object, metaclass=decorateAll(Counter)):

    def __init__(self, initial_amount):
        self.amount = initial_amount

    def withdraw(self, towithdraw):
        self.amount -= towithdraw

    def deposit(self, todeposit):
        self.amount += todeposit

    def balance(self):
        return self.amount

Кажется, все отлично работает, когда я передаю в метакласс декоратора декоратор, реализованный следующим образом:

def Counter(fun):
    fun.count = 0
    def wrapper(*args):
        fun.count += 1
        print("{0} Executed {1} times".format(fun.__name__, fun.count))
        return fun(*args)
    return wrapper

Но когда я использую декоратор, реализованный таким образом:

class Counter():

    def __init__(self, fun):
        self.fun = fun
        self.count = 0

    def __call__(self, *args, **kwargs):
        print("args:", self, *args, **kwargs)
        self.count += 1
        print("{0} Executed {1} times".format(self.fun.__name__, self.count))
        return self.fun(*args, **kwargs)

Я получил эту ошибку:

line 32, in __call__
return self.fun(*args, **kwargs)
TypeError: __init__() missing 1 required positional argument: 'initial_amount'

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


person Nikaido    schedule 22.01.2016    source источник


Ответы (1)


Вам нужно реализовать Counter как вызываемый дескриптор. Когда __get__ выполняется для дескриптора, вы имитируете привязку дескриптора к переданному ему экземпляру. Плюс сохранение количества для каждого метода/объекта.

Этот код:

import collections
import functools
import types


def decorateAll(decorator):
    class MetaClassDecorator(type):

        def __new__(meta, classname, supers, classdict):
            for name, elem in classdict.items():
                if type(elem) is types.FunctionType:
                    classdict[name] = decorator(classdict[name])
            return type.__new__(meta, classname, supers, classdict)
    return MetaClassDecorator


class Counter(object):
    def __init__(self, fun):
        self.fun = fun
        self.cache = {None: self}
        self.count = collections.defaultdict(int)

    def __get__(self, obj, cls=None):
        if obj is None:
            return self

        try:
            return self.cache[obj]
        except KeyError:
            pass

        print('Binding {} and {}'.format(self.fun, obj))
        cex = self.cache[obj] = functools.partial(self.__call__, obj)
        return cex

    def __call__(self, obj, *args, **kwargs):
        print("args:", obj, *args, **kwargs)
        self.count[obj] += 1
        print("{0} Exec {1} times".format(self.fun.__name__, self.count[obj]))
        return self.fun(obj, *args, **kwargs)


class Account(object, metaclass=decorateAll(Counter)):

    def __init__(self, initial_amount):
        self.amount = initial_amount

    def withdraw(self, towithdraw):
        self.amount -= towithdraw

    def deposit(self, todeposit):
        self.amount += todeposit

    def balance(self):
        return self.amount


a = Account(33.5)

print(a.balance())

Производит следующий вывод:

Binding <function Account.__init__ at 0x000002250BCD8B70> and <__main__.Account object at 0x000002250BCE8BE0>
args: <__main__.Account object at 0x000002250BCE8BE0> 33.5
__init__ Exec 1 times
Binding <function Account.balance at 0x000002250BCD8D90> and <__main__.Account object at 0x000002250BCE8BE0>
args: <__main__.Account object at 0x000002250BCE8BE0>
balance Exec 1 times
33.5

Который вызывает метод __call__ дескриптора, сохраняет количество для каждого метода, имитируя привязку, создающую объект типа functools.partial.

person mementum    schedule 22.01.2016
comment
когда, по вашему мнению, более удобно использовать этот подход? - person Nikaido; 23.01.2016
comment
В вашем случае это уместно, потому что вы декорируете обычные методы (те, которые принимают self в качестве первого параметра, т.е. они привязаны к экземпляру во время создания экземпляра). Альтернативой является возврат другого объекта (а не functools.partial), который содержит ссылку на экземпляр (obj) и связан с дескриптором. - person mementum; 23.01.2016