Как украсить класс?

129

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

Ищете что-то вроде следующего (с синтаксической ошибкой в ​​'class 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()

Я думаю, что мне действительно нужен способ сделать что-то вроде интерфейса C # в Python. Полагаю, мне нужно сменить парадигму.

Роберт Гоуленд
источник

Ответы:

80

Я бы поддержал идею о том, что вы, возможно, пожелаете рассмотреть подкласс вместо описанного вами подхода. Однако, не зная вашего конкретного сценария, 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

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

Джаррет Харди
источник
Спасибо, это то, что я ищу. Класс, который может изменять любое количество других классов, чтобы все они имели определенный член. Мои причины, по которым классы не наследуются от общего класса идентификаторов, заключаются в том, что я хочу иметь версии классов без идентификаторов, а также версии идентификаторов.
Роберт Гоуленд,
Раньше метаклассы были лучшим способом делать такие вещи в Python2.5 или более ранней версии, но в настоящее время вы можете очень часто использовать декораторы классов (см. Ответ Стивена), которые намного проще.
Джонатан Хартли
203

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

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

@addID
class Foo:
    pass

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

class Foo:
    pass

Foo = addID(Foo)

Однако обратите внимание, что это работает так же, как для декораторов функций, и что декоратор должен возвращать новый (или измененный исходный) класс, что не является тем, что вы делаете в примере. Декоратор 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

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

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

Стивен
источник
2
... хотя в вопросе конкретно упоминается Python 2.5 :-)
Джаррет Харди,
5
Извините за путаницу, но примеры кода в комментариях не очень хороши ...: def addID (original_class): original_class .__ orig__init__ = original_class .__ init__ def init __ (self, * args, ** kws): print "decorator" self.id = 9 original_class .__ orig__init __ (self, * args, ** kws) original_class .__ init = init return original_class @addID class Foo: def __init __ (self): print "Foo" a = Foo () print a.id
Gerald Senarclens de Grancy
2
@Steven, @Gerald прав, если бы вы могли обновить свой ответ, было бы намного легче читать его код;)
День
3
@Day: спасибо за напоминание, я раньше не замечал комментариев. Однако: я также получаю превышение максимальной глубины рекурсии с кодом Джеральда. Попробую сделать рабочую версию ;-)
Стивен
1
@Day и @Gerald: извините, проблема была не в коде Джеральда, я напортачил из-за искаженного кода в комментарии ;-)
Стивен
20

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

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

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

class Foo:
    pass

FooId = addId(Foo)

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

@addId
class Foo:
    pass

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

Эндрю Кук
источник
4
Создание подклассов в декораторах опасно в Python 2.6+, поскольку прерывает superвызовы в исходном классе. Если Fooбы у метода был назван вызывающий его метод foo, super(Foo, self).foo()он бы рекурсивно повторялся бесконечно, потому что имя Fooпривязано к подклассу, возвращаемому декоратором, а не к исходному классу (который недоступен по любому имени). Отсутствие аргументов в Python 3 super()позволяет избежать этой проблемы (я предполагаю, что с помощью той же магии компилятора, которая позволяет ему вообще работать). Вы также можете обойти проблему, вручную украсив класс под другим именем (как вы это делали в примере Python 2.5).
Blckknght
1
да. спасибо, понятия не имел (я использую python 3). добавлю комментарий.
Эндрю Кук
13

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

Загляните в документацию класса .

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

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)

Таким образом, у Босса есть все Employee, в том числе его собственный __init__метод и собственные участники.

mpeterson
источник
3
Думаю, я хотел, чтобы Босс не зависел от класса, который он содержит. То есть могут быть десятки разных классов, к которым я хочу применить функции Boss. Осталась ли у меня эта дюжина классов, унаследованных от Boss?
Роберт Гоуленд,
5
@ Роберт Гоуленд: Вот почему Python имеет множественное наследование. Да, вы должны наследовать различные аспекты от различных родительских классов.
S.Lott
7
@ S.Lott: В целом множественное наследование - плохая идея, даже слишком много уровней наследования тоже плохо. Я рекомендую вам держаться подальше от множественного наследования.
mpeterson,
5
mpeterson: Множественное наследование в Python хуже, чем этот подход? Что не так с множественным наследованием Python?
Арафангион,
2
@Arafangion: Множественное наследование обычно считается признаком неприятностей. Это создает сложные иерархии и сложные отношения. Если ваша проблемная область хорошо поддается множественному наследованию (можно ли ее моделировать иерархически?), Для многих это естественный выбор. Это относится ко всем языкам, допускающим множественное наследование.
Мортен Йенсен
6

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

Я нашел этот вопрос действительно удобным при декорировании классов, спасибо всем.

Вот еще пара примеров, основанных на других ответах, в том числе о том, как наследование влияет на вещи в 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

В assertпроверяет , что декоратор не используется без скобок. Если да, то декорируемый класс передается msgпараметру декоратора, который вызывает AssertionError.

@decorate_ifприменяется только decoratorесли имеет conditionзначение True.

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

Крис
источник
4

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

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

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

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

Очень умный.

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

Джордан Рейтер
источник
0

Вот пример, который отвечает на вопрос о возврате параметров класса. Более того, он по-прежнему соблюдает цепочку наследования, т.е. возвращаются только параметры самого класса. Функция get_paramsдобавлена ​​в качестве простого примера, но другие функции могут быть добавлены благодаря модулю inspect.

import inspect 

class Parent:
    @classmethod
    def get_params(my_class):
        return list(inspect.signature(my_class).parameters.keys())

class OtherParent:
    def __init__(self, a, b, c):
        pass

class Child(Parent, OtherParent):
    def __init__(self, x, y, z):
        pass

print(Child.get_params())
>>['x', 'y', 'z']
Патрисио Астудильо
источник