2012-01-23 1 views
6

У меня была интересная проблема сегодня утром. У меня был базовый класс, который выглядит следующим образом:Украшение метода, который уже является classmethod?

# base.py 
class Base(object): 

    @classmethod 
    def exists(cls, **kwargs): 
     # do some work 
     pass 

и модуль декоратора, который выглядел так:

# caching.py 

# actual caching decorator 
def cached(ttl): 
    # complicated 

def cached_model(ttl=300): 
    def closure(model_class): 
     # ... 
     # eventually: 
     exists_decorator = cached(ttl=ttl) 
     model_class.exists = exists_decorator(model_class.exists)) 

     return model_class 
    return closure 

Вот мой подкласс модель:

@cached_model(ttl=300) 
class Model(Base): 
    pass 

Дело в том, когда я на самом деле вызовите Model.exists, я получаю жалобы на неправильное количество аргументов! Проверка аргументов в декораторе показывает, что ничего странного не происходит - аргументы - именно то, что я ожидаю, и они совпадают с сигнатурой метода. Как добавить дополнительные декораторы к методу, который уже украшен classmethod?

Не все модели кэшированы, но метод exists() присутствует на каждой модели как метод класса, поэтому переопределение декораторов не является вариантом: cached_model может добавлять classmethod to exists(), но тогда что делает Существует() метод класса на нераскрытых моделях?

+1

Итак, какое решение? Не ясно. Было бы намного лучше, если бы вы оставили свой вопрос так, как есть, и отправили ответ. – Marcin

+1

Вы * можете * ... вопрос и ответ имейте это в виду, но будьте внимательны и ответьте отдельно. См. Http://meta.stackexchange.com/questions/17463/can-i-answer-my-own-questions-even-those-where-i-knew-the-answer-before-asking – delnan

+0

Я думаю, вы забыли '@ classmethod' в классе' Base'. –

ответ

1

Декоратор classmethod фактически добавляет аргумент class к вызовам метода при определенных обстоятельствах, насколько я могу судить, помимо привязки метода к классу.Решение редактировал мой класс закрытия украшения:

def cached_model(ttl=300): 
    def closure(model_class): 
     # ... 
     # eventually: 
     exists_decorator = cached(ttl=ttl, cache_key=exists_cache_key) 
     model_class.exists = classmethod(exists_decorator(model_class.exists.im_func)) 

     return model_class 
    return closure 

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

5

В Python, когда метод объявлен в теле функции, он точно подобен функции - после того, как класс проанализирован и существует, извлекая метод через «.». оператор преобразует эту функцию - на лету - в метод. Это преобразование действительно добавляет первый параметр к методу (если это не STATICMETHOD) -

так:

>>> class A(object): 
... def b(self): 
...  pass 
... 
>>> A.b is A.b 
False 

еще потому каждый при получении из «б» атрибут «A» дает другой экземпляр «метод объекта б»

>>> A.b 
<unbound method A.b> 

исходной функции «б» может быть получен без каких-либо trasnform, если один делает

>>> A.__dict__["b"] 
<function b at 0xe36230> 

Для функции украшенной @classmethod так же происходит, и значение «класс», добавляется в список параметров при его извлечении из А.

@classmethod The и @staticmethod декораторов будет обернуть основную функцию в другом дескрипторе чем обычный метод instance. Объект classmethod - то, что является функцией, когда она завернута с classmethod, является объектом дескриптора, который имеет метод «__get__», который возвращает функцию, обертывающую базовую функцию, и добавляет параметр «cls» перед всеми другими ,

Любой дополнительный декоратор для @classmethod должен «знать», он фактически имеет дело с объектом дескриптора, а не с функцией. -

>>> class A(object): 
... @classmethod 
... def b(cls): 
...  print b 
... 
>>> A.__dict__["b"] 
<classmethod object at 0xd97a28> 

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

+1

Правда, но иногда у вас нет контроля над порядком декораторов. В моем случае метод всегда является классом, но только некоторые классы имеют дополнительный декодер кеширования - он является подклассом. В этом случае вам нужен способ «отменить» первый декоратор класса. См. Мой ответ за то, что я закончил. –

2

Благодаря jsbueno для информации о Python. Я искал ответ на этот вопрос по делу decorating all methods of a class. Основываясь на поиске ответа на этот вопрос и réponse jsbueno, я был в состоянии собрать что-то вдоль линий:

def for_all_methods(decorator): 

    def decorate(cls): 

     for attr in dir(cls): 
      possible_method = getattr(cls, attr) 
      if not callable(possible_method): 
       continue 

      # staticmethod 
      if not hasattr(possible_method, "__self__"): 
       raw_function = cls.__dict__[attr].__func__ 
       decorated_method = decorator(raw_function) 
       decorated_method = staticmethod(decorated_method) 

      # classmethod 
      elif type(possible_method.__self__) == type: 
       raw_function = cls.__dict__[attr].__func__ 
       decorated_method = decorator(raw_function) 
       decorated_method = classmethod(decorated_method) 

      # instance method 
      elif possible_method.__self__ is None: 
       decorated_method = decorator(possible_method) 

      setattr(cls, attr, decorated_method) 

     return cls 
    return decorate 

Там немного избыточности и несколько вариаций вы могли бы использовать, чтобы расколоть это немного вниз.

0

Просто функциональный пример, чтобы добавить к великому ответу Скотта Лобделл в ...

messages.py

from distutils.cmd import Command 

import functools 
import unittest 

def for_all_methods(decorator): 

    def decorate(cls): 

     for attr in cls.__dict__: 
      possible_method = getattr(cls, attr) 
      if not callable(possible_method): 
       continue 

      # staticmethod 
      if not hasattr(possible_method, "__self__"): 
       raw_function = cls.__dict__[attr].__func__ 
       decorated_method = decorator(raw_function) 
       decorated_method = staticmethod(decorated_method) 

      # classmethod 
      if type(possible_method.__self__) == type: 
       raw_function = cls.__dict__[attr].__func__ 
       decorated_method = decorator(raw_function) 
       decorated_method = classmethod(decorated_method) 


      # instance method 
      elif possible_method.__self__ is None: 
       decorated_method = decorator(possible_method) 

      setattr(cls, attr, decorated_method) 

     return cls 

    return decorate 

def add_arguments(func): 
    """ 
    The add_arguments decorator simply add the passed in arguments 
    (args and kwargs) the returned error message. 
    """  
    @functools.wraps(func) 
    def wrapped(self, *args, **kwargs): 
     try: 
      message = func(self, *args, **kwargs) 
      message = ''.join([message, 
           "[ args:'", str(args), "'] ", 
           "[ kwargs:'", str(kwargs), "' ] " 
           ]) 
      return message 

     except Exception as e: 
      err_message = ''.join(["errorhandler.messages.MESSAGE: '", 
            str(func), 
            "(", str(args), str(kwargs), ")' ", 
            "FAILED FOR UNKNOWN REASON. ", 
            " [ ORIGINAL ERROR: ", str(e), " ] " 
            ]) 
      return err_message 

    return wrapped 



@for_all_methods(add_arguments)  
class MESSAGE(object): 
    """ 
      log.error(MSG.triggerPhrase(args, kwargs)) 

    """  
    @classmethod 
    def TEMPLATE(self, *args, **kwargs): 
     message = "This is a template of a pre-digested message." 
     return message 

использование

from messages import MESSAGE 

if __name__ == '__main__': 
    result = MESSAGE.TEMPLATE(1,2,test=3) 
    print result 

выход

This is a template of a pre-digested message.[ args:'(1, 2)'] [ kwargs:'{'test': 3}' ]