Як прикрасити клас?


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 :-)

Те, про що ти думаєш, - це метаклас. The__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

Заміна конструктора може бути дещо драматичною, але мова надає підтримку такого роду глибокої самоаналізу та динамічної модифікації.


Дякую, саме це я шукаю. Клас, який може змінювати будь-яку кількість інших класів таким чином, щоб усі вони мали певного члена. Мої причини відсутності успадкованих класів від загального класу ідентифікаторів полягають у тому, що я хочу мати версії класів, що не належать до ідентифікаторів, а також версії ID.
Роберт Гоуланд

Метакласи раніше були подібними способами робити такі речі в Python2.5 або старіших версіях, але сьогодні дуже часто можна використовувати декоратори класів (див. Відповідь Стівена), які набагато простіше.
Джонатан Хартлі

203

Крім питання, чи є декоратори класу правильним рішенням вашої проблеми:

У Python 2.6 та новіших версіях є декоративники класів з @ -syntax, тож ви можете писати:

@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 :-)
Jarret Hardie

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 клас Foo: def __init __ (self): надрукувати "Foo" a = Foo () print a.id
Джеральд Сенаркленс де Грансі

2
@Steven, @Gerald правий, якби ви могли оновити свою відповідь, було б набагато простіше прочитати його код;)
День

3
@Day: дякую за нагадування, я раніше не помічав коментарів. Однак: я отримую максимальну глибину рекурсії за допомогою Geralds коду. Спробую зробити робочу версію ;-)
Стівен

1
@Day and @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). додасть коментар.
andrew cooke

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__метод та власні члени.


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

5
@Robert Gowland: Ось чому Python має багаторазове успадкування. Так, вам слід успадкувати різні аспекти від різних батьківських класів.
С.Лотт

7
@ S.Lott: Загалом багатократне успадкування - це погана ідея, навіть занадто великий рівень спадкування поганий. Я порекомендую вам триматися подалі від багаторазового спадкування.
mpeterson

5
mpeterson: Чи багатократне успадкування 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

The assert перевіряє , що декоратор не використовується без дужок. Якщо він є, то клас, який декорується, передається msgпараметру декоратора, який піднімає AssertionError.

@decorate_if застосовується лише decorator якщо conditionоцінює до True.

Тест getattr, callableтест і @decorate_ifвикористовуються так, щоб декоратор не зламався, якщо foo()метод не існує для класу, який декорується.


4

Тут насправді досить хороша реалізація декоратора класу:

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

Я насправді думаю, що це досить цікава реалізація. Оскільки він підкласи класу, який він декорує, він буде вести себе точно так, як цей клас у таких речахisinstance чеки.

Це має додаткову перевагу: не рідкість __init__заява в користувальницькій формі джанго вносити зміни або доповнення до них, self.fieldsтак що краще, щоб зміни self.fieldsвідбувалися після всіх__init__ запущені для відповідного класу.

Дуже розумний.

Однак у вашому класі ви насправді хочете, щоб прикраса змінила конструктор, що, на мою думку, не є корисним випадком для декоратора класу.


0

Ось приклад, який відповідає на питання повернення параметрів класу. Більше того, він все ще поважає ланцюг успадкування, тобто повертаються лише параметри самого класу. Функція get_paramsдодається як простий приклад, але інші функції можна додати завдяки модулю перевірки.

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']

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.