2013-05-22 19 views
5

Chciałbym zawrzeć kilka metod klasowych w Pythonie z tym samym opakowaniem.Zawijanie wywołań do metod klasy Pythona

Koncepcyjnie to będzie wyglądać mniej więcej tak w najprostszym scenariuszu:

x = 0 # some arbitrary context 

class Base(object): 
    def a(self): 
     print "a x: %s" % x 

    def b(self): 
     print "b x: %s" % x 

class MixinWithX(Base): 
    """Wrap""" 
    def a(self): 
     global x 
     x = 1 
     super(MixinWithX, self).a() 
     x = 0 

    def b(self): 
     global x 
     x = 1 
     super(MixinWithX, self).a() 
     x = 0 

Oczywiście, gdy istnieje więcej sposobów niż a i b, to staje się bałagan. Wygląda na to, że powinno być coś prostszego. Oczywiście x mogą być modyfikowane w dekoratora ale nadal kończy się o długą listę śmieci, które zamiast powyższych wygląda następująco:

from functools import wraps 
def withx(f): 
    @wraps(f) # good practice 
    def wrapped(*args, **kwargs): 
     global x 
     x = 1 
     f(*args, **kwargs) 
     x = 0 
    return wrapped 

class MixinWithX(Base): 
    """Wrap""" 
    @withx 
    def a(self): 
     super(MixinWithX, self).a() 

    @withx 
    def b(self): 
     super(MixinWithX, self).b() 

Myślałem o użyciu __getattr__ w wstawek, ale oczywiście od metody, takie jako a i b są już zdefiniowane, to nigdy nie jest wywoływane.

Myślałem również o użyciu __getattribute__, ale zwraca atrybut, nie owijając połączenia. Przypuszczam, że __getattribute__ może zwrócić zamknięcie (przykład poniżej), ale nie jestem pewien, jak brzmi projekt. Oto przykład:

class MixinWithX(Base): 
    # a list of the methods of our parent class (Base) that are wrapped 
    wrapped = ['a', 'b'] 

    # application of the wrapper around the methods specified 
    def __getattribute__(self, name): 
     original = object.__getattribute__(self, name) 
     if name in wrapped: 
      def wrapped(self, *args, **kwargs): 
       global x 
       x = 1 # in this example, a context manager would be handy. 
       ret = original(*args, **kwargs) 
       x = 0 
       return ret 
      return wrapped 
     return original 

To przyszło mi do głowy, że może być coś wbudowane w Pythonie, które mogą zmniejszyć potrzebę ręcznego powielania każdą metodę klasy nadrzędnej, która ma być opakowane. A może zamknięcie w __getattribute__ jest właściwym sposobem na zrobienie tego. Byłbym wdzięczny za przemyślenia.

+0

Nie jestem całkiem pewien, czego chcesz, ale myślę, że szukasz dekoratorów. –

+0

@ Klasyczne: dekoratorzy nie pomagają. Dla jasności, szukam podklasy, która definiuje jako listę metody w klasie nadrzędnej, które mają zmienić konteksty przed i po wykonaniu. Metody z kolei nie musiałyby być indywidualnie definiowane w podklasie (a zatem nie byłoby miejsca dla dekoratora). –

Odpowiedz

4

Oto moja próba, która pozwala na bardziej zwięzłej składni ...

x = 0 # some arbitrary context 

# Define a simple function to return a wrapped class 
def wrap_class(base, towrap): 
    class ClassWrapper(base): 
     def __getattribute__(self, name): 
      original = base.__getattribute__(self, name) 
      if name in towrap: 
       def func_wrapper(*args, **kwargs): 
        global x 
        x = 1 
        try: 
         return original(*args, **kwargs) 
        finally: 
         x = 0 
       return func_wrapper 
      return original 
    return ClassWrapper 


# Our existing base class 
class Base(object): 
    def a(self): 
     print "a x: %s" % x 

    def b(self): 
     print "b x: %s" % x 


# Create a wrapped class in one line, without needing to define a new class 
# for each class you want to wrap. 
Wrapped = wrap_class(Base, ('a',)) 

# Now use it 
m = Wrapped() 
m.a() 
m.b() 

# ...or do it in one line... 
m = wrap_class(Base, ('a',))() 

... który wyprowadza ...

a x: 1 
b x: 0 
+0

Bardzo sprytny - byłby świetny podczas testowania jednostkowego. –

+0

Mogę dodać, że miałoby to tę zaletę, że pracowało nad metodami klasowymi. –

+1

@ BrianM.Hunt True. Podejście do patroszenia małp w zaakceptowanej odpowiedzi wydawało mi się przesadą, chociaż może być niewielka przewaga wydajności. Tak czy inaczej, jeśli ręcznie określasz podzbiór "atrybutów" do zawijania, to "inspect" jest prawie na pewno niepotrzebny. – Aya

1

Istnieje rozwiązanie, które nazywa się dekoratorem. Google "dekoratory pytonów" zawiera wiele informacji.

Podstawowym założeniem jest to, że dekorator jest funkcją, która wykonuje funkcję jako parametr i zwraca funkcję:

def decorate_with_x(f) 
    def inner(self): 
     self.x = 1 #you must always use self to refer to member variables, even if you're not decorating 
     f(self) 
     self.x = 0 
    return inner 

class Foo(object): 

    @decorate_with_x # @-syntax passes the function defined on next line 
         # to the function named s.t. it is equivalent to 
         # foo_func = decorate_with_x(foo_func) 
    def foo_func(self): 
     pass 
+0

Dzięki za wpis. Dekoratorzy nie rozwiązują problemu; zobacz edycję powyżej. –

+0

Aby zilustrować, dlaczego próbuję to rozwiązać w taki sposób, w jaki jestem, spróbuj napisać opakowanie funkcji "a" do "z", a następnie zrobić to dla dwóch lub trzech innych klas o zupełnie innych nazwach metod. To obrzydliwa ilość bezużytecznych powtórzeń. Musi być lepszy sposób. –

+0

@ BrianM.Hunt Widzę, że przyjąłeś odpowiedź, która proponuje dekoratorów jako rozwiązanie. – Marcin

2

Istnieją dwa ogólne kierunki mogę myśleć które są użyteczne w Twoim przypadku.

Jedna używa dekoratora klas . Napisz funkcję, która przyjmuje klasę i zwraca klasę z tym samym zestawem metod, dekorowaną (przez utworzenie nowej klasy przez wywołanie type(...) lub przez zmianę klasy wejściowej w miejscu).

EDYCJA: (Rzeczywisty kod pakowania/kontroli, który miałem na myśli jest podobny do , co @girasquid ma w swojej odpowiedzi, ale łączenie odbywa się za pomocą dekoracji zamiast mixin/dziedziczenia, co moim zdaniem jest bardziej elastyczne i solidne.)

Który prowadzi mnie do drugiej opcji, która ma używać metaklasy , która może być czystsza (jeszcze trudniejsza, jeśli nie jesteś przyzwyczajony do pracy z metaclasses). Jeśli nie masz dostępu do definicji oryginalnej klasy lub nie chcesz zmieniać oryginalnej definicji, możesz podklasować oryginalną klasę, ustawiając metaclass na pochodnej.

3

Można to zrobić za pomocą dekoratorów i inspect:

from functools import wraps 
import inspect 

def withx(f): 
    @wraps(f) 
    def wrapped(*args, **kwargs): 
     print "decorator" 
     x = 1 
     f(*args, **kwargs) 
     x = 0 
    return wrapped 

class MyDecoratingBaseClass(object): 
    def __init__(self, *args, **kwargs): 
     for member in inspect.getmembers(self, predicate=inspect.ismethod): 
      if member[0] in self.wrapped_methods: 
       setattr(self, member[0], withx(member[1])) 

class MyDecoratedSubClass(MyDecoratingBaseClass): 
    wrapped_methods = ['a', 'b'] 
    def a(self): 
     print 'a' 

    def b(self): 
     print 'b' 

    def c(self): 
     print 'c' 

if __name__ == '__main__': 
    my_instance = MyDecoratedSubClass() 
    my_instance.a() 
    my_instance.b() 
    my_instance.c() 

wyjściowa:

decorator 
a 
decorator 
b 
c 
+0

Bardzo zadbane - świetna odpowiedź! –

+0

Jeśli mamy zamiar monkeypatch, to dlaczego nie wystarczy uruchomić 'dla metody in wrapped_methods: setattr (self, method, withx (getattr (self, method)))'? Nie trzeba w ogóle "sprawdzać". –

+0

Chciałbym zaznaczyć, że ta odpowiedź, w przeciwieństwie do rozwiązania Aya, nie będzie działać dla metod klasowych, ponieważ '__init__' nie zostanie wywołany i nie ma żadnego przykładu dla monkeypatch. –

Powiązane problemy