2009-03-25 5 views
98

В Python 2.5 есть ли способ создать декоратор, который украшает класс? В частности, я хочу использовать декоратор для добавления члена в класс и изменения конструктора, чтобы принять значение для этого элемента.Декоратор класса Python

Ищете что-то вроде следующего (который имеет синтаксическую ошибку 'класса Foo:':

def getId(self): return self.__id 

class addID(original_class): 
    def __init__(self, id, *args, **kws): 
     self.__id = id 
     self.getId = getId 
     original_class.__init__(self, *args, **kws) 

@addID 
class Foo: 
    def __init__(self, value1): 
     self.value1 = value1 

if __name__ == '__main__': 
    foo1 = Foo(5,1) 
    print foo1.value1, foo1.getId() 
    foo2 = Foo(15,2) 
    print foo2.value1, foo2.getId() 

Спасибо, Роб

Edit: Перечитывая этот вопрос, я думаю, что Я действительно хочу сделать что-то вроде интерфейса C# в Python. Мне нужно переключить мою парадигму, я полагаю.

+34

Хотя это полезно, декораторы Python отличаются от декоратора. Похоже, неудачное имя. Однако я могу пойти так. Благодарю. –

+13

@Robert Gowland: Очень дипломатично. –

ответ

65

Я бы предпочел, чтобы вы могли рассмотреть подкласс вместо подхода, который вы наметили. Однако, не зная вашего конкретного сценария, YMMV :-)

Что вы думаете о метаклассе. Функция __new__ в метаклассе передает полное предложенное определение класса, которое затем может быть переписано до создания класса. Вы можете в это время добавить конструктор для нового.

Пример:

def substitute_init(self, id, *args, **kwargs): 
    pass 

class FooMeta(type): 

    def __new__(cls, name, bases, attrs): 
     attrs['__init__'] = substitute_init 
     return super(FooMeta, cls).__new__(cls, name, bases, attrs) 

class Foo(object): 

    __metaclass__ = FooMeta 

    def __init__(self, value1): 
     pass 

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

+0

Спасибо, это то, что я ищу. Класс, который может изменять любое количество других классов, чтобы все они имели определенный член. Мои причины не иметь классов, наследуемых от общего класса ID, это то, что я хочу иметь не-идентификационные версии классов, а также идентификационные версии. –

+0

Метаклассы раньше были способными делать подобные вещи в Python2.5 или старше, но в наши дни вы можете очень часто использовать декораторы классов (см. Ответ Стивена), которые намного проще. –

11

Это нехорошая практика, и нет никакой механики из-за этого. Правильный способ выполнить то, что вы хотите, - это наследование.

Взгляните на class documentation.

Небольшой пример:

class Employee(object): 

    def __init__(self, age, sex, siblings=0): 
     self.age = age 
     self.sex = sex  
     self.siblings = siblings 

    def born_on(self):  
     today = datetime.date.today() 

     return today - datetime.timedelta(days=self.age*365) 


class Boss(Employee):  
    def __init__(self, age, sex, siblings=0, bonus=0): 
     self.bonus = bonus 
     Employee.__init__(self, age, sex, siblings) 

Таким образом Boss имеет все Employee имеет, с также своими __init__ метод и собственных членов.

+3

Я предполагаю, что я хотел иметь агностик босса класса, который он содержит. То есть, могут быть десятки разных классов, к которым я хочу применить функции Boss. Разве я ушел с тем, что эти дюжины классов наследуются от Босса? –

+4

@Robert Gowland: Вот почему Python имеет множественное наследование. Да, вы должны наследовать различные аспекты из разных родительских классов. –

+7

@ S.Lott: В общем, множественное наследование - плохая идея, слишком много уровней наследования тоже плохо. Я рекомендую вам держаться подальше от множественного наследования. – mpeterson

176

Помимо вопроса класса декораторов, являются ли правильное решение вашей проблемы:

в Python 2.6 и выше, есть класс декораторов с @ -syntax, так что вы можете написать:

@addID 
class Foo: 
    pass 

в более ранних версиях, вы можете сделать это по-другому:

class Foo: 
    pass 

Foo = addID(Foo) 

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

def addID(original_class): 
    orig_init = original_class.__init__ 
    # make copy of original __init__, so we can call it without recursion 

    def __init__(self, id, *args, **kws): 
     self.__id = id 
     self.getId = getId 
     orig_init(self, *args, **kws) # call the original __init__ 

    original_class.__init__ = __init__ # set the class' __init__ to the new one 
    return original_class 

Вы можете затем использовать соответствующий синтаксис для питона версии, как описано выше.

Но я согласен с другими, что наследование лучше подходит, если вы хотите переопределить __init__.

+2

... даже если в вопросе конкретно упоминается Python 2.5 :-) –

+0

Ваш пример вызывает превышение глубины рекурсии RuntimeError: поскольку при создании экземпляра класса initial_class .__ init__ - это функция, которая вызывается, поэтому имеет сам вызов __init__. В следующем комментарии я опубликую рабочий пример. –

+5

Извините за linesep беспорядке, но код образцы не являются строго большим в комментариях ...: Защиту ADDID (original_class): original_class .__ orig__init__ = original_class .__ init__ Защиту __init __ (самоповреждения, * Args, ** KWS): печать "декоратор" self.id = 9 original_class .__ orig__init __ (самоповреждения, * арг, ** KWS) original_class .__ init__ = __init__ возвращение original_class @addID класса Foo: четкости __init __ (сам): печать "Foo" a = Foo() print a.id –

4

Там на самом деле очень хорошая реализация класса декоратора здесь:

https://github.com/agiliq/Django-parsley/blob/master/parsley/decorators.py

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

Он имеет дополнительное преимущество: это не редкость для __init__ заявления в пользовательских Джанго форме вносить изменения или дополнения в self.fields так что лучше для изменений в self.fields, чтобы случиться после все из __init__ поработает класс в вопрос.

Очень умный.

Тем не менее, в вашем классе вы действительно хотите, чтобы украшение изменяло конструктор, который, как я считаю, не является хорошим вариантом для декоратора класса.

10

Никто не объяснил, что вы можете динамически определять классы. так что вы можете иметь декоратора, который определяет (и возвращает) подкласс:

def addId(cls): 

    class AddId(cls): 

     def __init__(self, id, *args, **kargs): 
      super(AddId, self).__init__(*args, **kargs) 
      self.__id = id 

     def getId(self): 
      return self.__id 

    return AddId 

, который может быть использован в питоне 2 (комментарий от Blckknght, который объясняет, почему вы должны продолжать делать это в 2.6+), как это:

class Foo: 
    pass 

FooId = addId(Foo) 

и в Python 3, как это (но будьте осторожны, чтобы использовать super() в классах):

@addId 
class Foo: 
    pass 

так что вы можете иметь свой кусок пирога в nd eat it - наследование и декораторы!

+2

Подкласс в декораторе опасен в Python 2.6+, потому что он прерывает вызовы 'super' в исходном классе. Если 'Foo' имел метод с именем' foo', который назывался 'super (Foo, self) .foo()', он возвращался бы бесконечно, потому что имя 'Foo' связано с подклассом, возвращаемым декоратором, а не с исходным классом (который не доступен никаким именем). Недопустимый 'super()' Python 3 позволяет избежать этой проблемы (я предполагаю, используя ту же магию компилятора, которая позволяет ей вообще работать). Вы также можете обойти проблему, вручную украсив класс под другим именем (как и в примере Python 2.5). – Blckknght

+1

huh. спасибо, я понятия не имел (я использую python 3). добавит комментарий. –

0

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

Я нашел этот вопрос действительно удобным, хотя и украшал классы, спасибо всем. Вот еще несколько примеров, на основе других ответов, в том числе, как наследование влияет на вещи в Python 2.7, (и @wraps, который поддерживает строку документации исходной функции, и т.д):

def dec(klass): 
    old_foo = klass.foo 
    @wraps(klass.foo) 
    def decorated_foo(self, *args ,**kwargs): 
     print('@decorator pre %s' % msg) 
     old_foo(self, *args, **kwargs) 
     print('@decorator post %s' % msg) 
    klass.foo = decorated_foo 
    return klass 

@dec # no parentheses 
class Foo... 

Часто вы хотите добавить параметры вашего декоратора:

from functools import wraps 

def dec(msg='default'): 
    def decorator(klass): 
     old_foo = klass.foo 
     @wraps(klass.foo) 
     def decorated_foo(self, *args ,**kwargs): 
      print('@decorator pre %s' % msg) 
      old_foo(self, *args, **kwargs) 
      print('@decorator post %s' % msg) 
     klass.foo = decorated_foo 
     return klass 
    return decorator 

@dec('foo decorator') # you must add parentheses now, even if they're empty 
class Foo(object): 
    def foo(self, *args, **kwargs): 
     print('foo.foo()') 

@dec('subfoo decorator') 
class SubFoo(Foo): 
    def foo(self, *args, **kwargs): 
     print('subfoo.foo() pre') 
     super(SubFoo, self).foo(*args, **kwargs) 
     print('subfoo.foo() post') 

@dec('subsubfoo decorator') 
class SubSubFoo(SubFoo): 
    def foo(self, *args, **kwargs): 
     print('subsubfoo.foo() pre') 
     super(SubSubFoo, self).foo(*args, **kwargs) 
     print('subsubfoo.foo() post') 

SubSubFoo().foo() 

Выходы:

@decorator pre subsubfoo decorator 
subsubfoo.foo() pre 
@decorator pre subfoo decorator 
subfoo.foo() pre 
@decorator pre foo decorator 
foo.foo() 
@decorator post foo decorator 
subfoo.foo() post 
@decorator post subfoo decorator 
subsubfoo.foo() post 
@decorator post subsubfoo decorator 

Я использовал функцию декоратора, как я нахожу их более кратким. Вот класс для украшения класса:

class Dec(object): 

    def __init__(self, msg): 
     self.msg = msg 

    def __call__(self, klass): 
     old_foo = klass.foo 
     msg = self.msg 
     def decorated_foo(self, *args, **kwargs): 
      print('@decorator pre %s' % msg) 
      old_foo(self, *args, **kwargs) 
      print('@decorator post %s' % msg) 
     klass.foo = decorated_foo 
     return klass 

Более надежная версия, которая проверяет эти скобки, и работает, если методы не существуют на украшенном классе:

from inspect import isclass 

def decorate_if(condition, decorator): 
    return decorator if condition else lambda x: x 

def dec(msg): 
    # Only use if your decorator's first parameter is never a class 
    assert not isclass(msg) 

    def decorator(klass): 
     old_foo = getattr(klass, 'foo', None) 

     @decorate_if(old_foo, wraps(klass.foo)) 
     def decorated_foo(self, *args ,**kwargs): 
      print('@decorator pre %s' % msg) 
      if callable(old_foo): 
       old_foo(self, *args, **kwargs) 
      print('@decorator post %s' % msg) 

     klass.foo = decorated_foo 
     return klass 

    return decorator 

The assert проверки, декоратор не использовался без круглых скобок.Если это так, то класс, который оформляется, передается в параметр msg декоратора, который поднимает AssertionError.

@decorate_if применяется только к decorator, если condition - True.

getattr, callable тест, и @decorate_if используются так, что декоратор не ломается, если метод foo() не существует на класс украшаются.

Смежные вопросы