Jak udekorować klasę?

133

Czy w Pythonie 2.5 jest sposób na stworzenie dekoratora, który zdobi klasę? W szczególności chcę użyć dekoratora, aby dodać element członkowski do klasy i zmienić konstruktora, aby przyjmował wartość dla tego elementu.

Szukam czegoś podobnego do następującego (który ma błąd składni w „klasie 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()

Myślę, że naprawdę szukam sposobu na zrobienie czegoś w rodzaju interfejsu C # w Pythonie. Przypuszczam, że muszę zmienić mój paradygmat.

Robert Gowland
źródło

Odpowiedzi:

80

Poparłbym pogląd, że możesz chcieć rozważyć podklasę zamiast podejścia, które nakreśliłeś. Jednak nie znając twojego konkretnego scenariusza, YMMV :-)

Myślisz o metaklasie. __new__Funkcja w metaklasą przechodzi pełną Proponowana definicja klasy, która może następnie przepisać przed utworzeniem klasy. Możesz w tym czasie zastąpić konstruktora nowym.

Przykład:

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

Zastąpienie konstruktora jest być może nieco dramatyczne, ale język zapewnia obsługę tego rodzaju głębokiej introspekcji i dynamicznej modyfikacji.

Jarret Hardie
źródło
Dziękuję, tego właśnie szukam. Klasa, która może modyfikować dowolną liczbę innych klas, tak aby wszystkie miały określonego członka. Moje powody, dla których klasy nie dziedziczą po wspólnej klasie ID, to fakt, że chcę mieć wersje klas bez identyfikatora, a także wersje ID.
Robert Gowland,
Metaklasy były kiedyś sposobem na robienie takich rzeczy w Pythonie 2.5 lub starszym, ale obecnie bardzo często można używać dekoratorów klas (patrz odpowiedź Stevena), które są znacznie prostsze.
Jonathan Hartley
205

Oprócz pytania, czy klasowe dekoratory są właściwym rozwiązaniem Twojego problemu:

W Pythonie 2.6 i nowszych istnieją dekoratory klas z @ -syntax, więc możesz napisać:

@addID
class Foo:
    pass

W starszych wersjach możesz to zrobić w inny sposób:

class Foo:
    pass

Foo = addID(Foo)

Zauważ jednak, że działa to tak samo, jak w przypadku dekoratorów funkcji i że dekorator powinien zwrócić nową (lub zmodyfikowaną oryginalną) klasę, co nie jest tym, co robisz w przykładzie. Dekorator addID wyglądałby tak:

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

Możesz wtedy użyć odpowiedniej składni dla swojej wersji Pythona, jak opisano powyżej.

Ale zgadzam się z innymi, że dziedziczenie jest lepsze, jeśli chcesz zastąpić __init__.

Steven
źródło
2
... chociaż pytanie konkretnie wspomina o Pythonie 2.5 :-)
Jarret Hardie
5
Przepraszamy za bałagan w wierszach, ale próbki kodu nie są zbyt dobre w komentarzach ...: 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 ma rację, gdybyś mógł zaktualizować swoją odpowiedź, o wiele łatwiej byłoby odczytać jego kod;)
Dzień
3
@Day: dzięki za przypomnienie, wcześniej nie zauważyłem komentarzy. Jednak: Otrzymuję również maksymalną głębokość rekurencji przekroczoną w kodzie Geraldsa. Spróbuję stworzyć działającą wersję ;-)
Steven,
1
@Day i @Gerald: przepraszam, problem nie dotyczył kodu Geralda, zawiodłem z powodu zniekształconego kodu w komentarzu ;-)
Steven
20

Nikt nie wyjaśnił, że możesz dynamicznie definiować klasy. Możesz więc mieć dekorator, który definiuje (i zwraca) podklasę:

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

Którego można użyć w Pythonie 2 (komentarz Blckknght, który wyjaśnia, dlaczego powinieneś kontynuować to w 2.6+) w następujący sposób:

class Foo:
    pass

FooId = addId(Foo)

A w Pythonie 3 takim jak ten (ale uważaj, aby używać super()w swoich klasach):

@addId
class Foo:
    pass

Możesz więc mieć swoje ciasto i je zjeść - spadek i dekoratorzy!

Andrew Cooke
źródło
4
Tworzenie podklas w dekoratorze jest niebezpieczne w Pythonie 2.6+, ponieważ przerywa superwywołania w oryginalnej klasie. Gdyby Foometoda o nazwie, fooktóra super(Foo, self).foo()ją wywołała, powtarzałaby się w nieskończoność, ponieważ nazwa Foojest powiązana z podklasą zwracaną przez dekorator, a nie z klasą oryginalną (do której żadna nazwa nie jest dostępna). Python 3 bez argumentów super()unika tego problemu (zakładam, że za pomocą tej samej magii kompilatora, która pozwala mu w ogóle działać). Możesz również obejść ten problem, ręcznie dekorując klasę pod inną nazwą (tak jak w przykładzie z Pythonem 2.5).
Blckknght
1
hm. dzięki, nie miałem pojęcia (używam Pythona 3). doda komentarz.
Andrew Cooke
13

To nie jest dobra praktyka i dlatego nie ma takiego mechanizmu. Właściwym sposobem na osiągnięcie tego, czego chcesz, jest dziedziczenie.

Zapoznaj się z dokumentacją zajęć .

Mały przykład:

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)

W ten sposób Boss ma wszystko Employee, co ma, również własną __init__metodę i własnych członków.

mpeterson
źródło
3
Chyba chciałem mieć agnostyka Bossa w stosunku do klasy, którą zawiera. Oznacza to, że mogą istnieć dziesiątki różnych klas, do których chcę zastosować funkcje Boss. Czy zostaje mi posiadanie tych tuzinów klas odziedziczonych po Boss?
Robert Gowland,
5
@Robert Gowland: Dlatego Python ma wielokrotne dziedziczenie. Tak, powinieneś dziedziczyć różne aspekty z różnych klas nadrzędnych.
S.Lott,
7
@ S.Lott: Generalnie dziedziczenie wielokrotne jest złym pomysłem, nawet zbyt duże poziomy dziedziczenia też są złe. Radzę ci trzymać się z dala od wielokrotnego dziedziczenia.
mpeterson
5
mpeterson: Czy dziedziczenie wielokrotne w Pythonie jest gorsze niż to podejście? Co jest nie tak z dziedziczeniem wielokrotnym w Pythonie?
Arafangion
2
@Arafangion: Dziedziczenie wielokrotne jest ogólnie uważane za znak ostrzegawczy. Tworzy złożone hierarchie i trudne do zrozumienia relacje. Jeśli twoja domena, w której występuje problem, dobrze nadaje się do dziedziczenia wielokrotnego (czy można ją modelować hierarchicznie?), Dla wielu jest to naturalny wybór. Dotyczy to wszystkich języków, które umożliwiają wielokrotne dziedziczenie.
Morten Jensen,
7

Zgadzam się, że dziedziczenie lepiej pasuje do postawionego problemu.

Uważam, że to pytanie jest bardzo przydatne na zajęciach dekorowania, dzięki wszystkim.

Oto kilka innych przykładów, opartych na innych odpowiedziach, w tym jak dziedziczenie wpływa na rzeczy w Pythonie 2.7 (i @wraps , który zachowuje oryginalną dokumentację funkcji itp.):

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...

Często chcesz dodać parametry do swojego dekoratora:

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

Wyjścia:

@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

Użyłem dekoratora funkcji, ponieważ uważam je za bardziej zwięzłe. Oto klasa do dekoracji klasy:

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

Bardziej niezawodna wersja, która sprawdza te nawiasy i działa, jeśli metody nie istnieją w ozdobionej klasie:

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

Do assertsprawdza, czy dekorator nie został wykorzystany bez nawiasów. Jeśli tak, to dekorowana klasa jest przekazywana do msgparametru dekoratora, który podnosi wartość AssertionError.

@decorate_ifstosuje tylko decoratorif conditionszacuje do True.

Metody getattr, callabletest i @decorate_ifsą używane, aby dekorator nie zepsuł się, jeśli foo()metoda nie istnieje w dekorowanej klasie.

Chris
źródło
4

W rzeczywistości jest tutaj całkiem dobra implementacja dekoratora klas:

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

Właściwie uważam, że to całkiem ciekawa realizacja. Ponieważ jest podklasą klasy, którą ozdabia, będzie zachowywał się dokładnie tak, jak ta klasa, na przykład w postaci isinstanceczeków.

Ma dodatkową zaletę: nierzadko zdarza się, że __init__instrukcja w niestandardowym formularzu django wprowadza modyfikacje lub dodatki, self.fieldswięc lepiej jest, aby zmiany self.fieldsnastąpiły po wykonaniu wszystkich czynności __init__dla danej klasy.

Bardzo mądry.

Jednak w swojej klasie chcesz, aby dekoracja zmieniała konstruktora, co nie wydaje mi się dobrym przypadkiem użycia dla dekoratora klas.

Jordan Reiter
źródło
0

Oto przykład, który odpowiada na pytanie o zwracanie parametrów klasy. Co więcej, nadal szanuje łańcuch dziedziczenia, tj. Zwracane są tylko parametry samej klasy. Funkcja get_paramsjest dodana jako prosty przykład, ale inne funkcjonalności można dodać dzięki modułowi 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']

Patricio Astudillo
źródło