2016-11-04 15 views
7

Mam klasy abstrakcyjnej z trzema metodami, które są równoważne sensie - wszystkie można zdefiniować w kategoriach nawzajem za pomocą drogich funkcji konwersji. Chcę móc napisać klasy pochodnej, która musiałaby tylko zastąpić jedną z metod i automatycznie uzyskać pozostałe dwa. PrzykładJak zdefiniować trzy metody cyklicznie?

class FooBarBaz(object): 
    def foo(self, x): 
     return foo_from_bar(self.bar(x)) 
     # OR return foo_from_baz(self.baz(x)) 

    def bar(self, x): 
     return bar_from_foo(self.foo(x)) 
     # OR return bar_from_baz(self.baz(x)) 

    def baz(self, x): 
     return baz_from_bar(self.bar(x)) 
     # OR return baz_from_foo(self.foo(x)) 

class Derived1(FooBarBaz): 
    def bar(self, x): 
     return 5 
     # at this point foo = foo_from_bar(5) and 
     # baz = baz_from_bar(5), which is what I wanted 

class Derived2(FooBarBaz): 
    def foo(self, x): 
     return 6 
     # at this point bar = bar_from_foo(6) and 
     # baz = baz_from_bar(bar_from_foo(6)), 
     # which is not ideal, but still works 

class Derived3(FooBarBaz): 
    def baz(self, x): 
     return 7 
     # at this point foo and bar remain defined 
     # in terms of each other, which is a PROBLEM 

Wiem, że mogę wyraźnie powiedzieć każdej pochodnej klasy, których konwersji użyć. Chcę wiedzieć, czy istnieje sposób, aby klasa rodzicielska mogła to samodzielnie rozwiązać, nie modyfikując dzieci.

+1

Jak widać w poniższych odpowiedziach, decyzja, które podejście zastosować, zależy od tego, jak kosztowne są konwersje, a także czy masz tylko trzy alternatywy, czy może 20. Te ostatnie w połączeniu z unikaniem wielokrotnych konwersji w razie potrzeby dałoby mi niezłe ćwiczenie :-D. –

Odpowiedz

3

Można użyć metaprogramming technik, takich jak pisanie metaclass, który wypełnia automatycznie pozostałe metody, lub użyć introspekcji, aby spojrzeć na klasy w type(self).mro() z kolei, aby dowiedzieć się, które metody zostały nadpisane. Jednak te opcje zdecydowanie należą do kategorii "za dużo magii" dla mnie, więc pójdę z czymś prostszym.

Po prostu podziel każdą z metod na dwie: jedną ogólną i rzeczywistą implementację. Klasy pochodne zastąpić rzeczywistej realizacji:

class FooBarBaz(object): 

    def foo_impl(self, x): 
     raise NotImplementedError 

    def foo(self, x): 
     try: 
      return self.foo_impl(x) 
     except NotImplementedError: 
      try: 
       return foo_from_bar(self.bar_impl(x)) 
      except NotImplementedError: 
       return foo_from_baz(self.baz_impl(x)) 

    # Similarly fo bar and baz 

class Dervied(FooBarBaz): 

    def bar_impl(self, x): 
     return 5 

Wspólna logika może być również uwzględnione w dekoratora:

def first_implemented(func): 
    @functools.wraps 
    def wrapper(*args, **kwargs): 
     for f in func(*args, **kwargs): 
      try: 
       return f() 
      except NotImplementedError: 
       pass 
     raise NotImplementedError 
    return wrapper 

class FooBarBaz(object): 

    def foo_impl(self, x): 
     raise NotImplementedError 

    @first_implemented 
    def foo(self, x): 
     yield lambda: self.foo_impl(x) 
     yield lambda: foo_from_bar(self.bar_impl(x)) 
     yield lambda: foo_from_baz(self.baz_impl(x)) 
+0

Poszedłem na nieco bardziej magiczne rozwiązanie (kto nie lubi magii?), Ale twój pomysł jest sprytny. –

+0

@ KarolisJuodelė Magia ma tendencję do gryzienia cię (lub kogoś innego) podczas próby rozszerzenia lub refaktoryzacji kodu w przyszłości. Zapamiętaj moje słowa. –

+0

Poszerzyłem sposób implementacji wspomnianego dekoratora. –

2

Proponuję zdefiniować konwersje w czystym kole, tak aby przedefiniowanie jednej metody przerwie ten okrąg. Czyli:

class FooBarBaz(object): 
    def foo(self, x): 
     return foo_from_baz(self.baz(x)) 

    def bar(self, x): 
     return bar_from_foo(self.foo(x)) 

    def baz(self, x): 
     return baz_from_bar(self.bar(x)) 

Zmieniano jednocześnie z komentarzem na tej samej kwestii podniesionych: które powinny działać, ale oczywiście czasami trzeba konwertować dwukrotnie. Aby tego uniknąć, można zaimplementować jakąś leniwą ocenę, tj. Funkcje najpierw generują obiekt, który wie, jak oceniać samą siebie, ale najpierw, gdy zapytamy o jej wartość, operacja jest wykonywana. W ten sposób łańcuch konwersji można uprościć przed oceną, jeśli jest on drogi.

+1

OP stwierdził, że funkcje konwersji są drogie, więc to rozwiązanie nie jest optymalne, ponieważ wymaga dwóch funkcji konwersji, w których wystarczy. –

1

Jestem rzeczywiście powielania Dr. V's answer. Jestem delegowania moją odpowiedź, że usprawiedliwia trud że spędziłem pisania kodu:

#!/usr/bin/env python 
def foo_from_bar(x): 
    return 'foo_from_bar(%s)' % x 

def bar_from_baz(x): 
    return 'bar_from_baz(%s)' % x 

def baz_from_foo(x): 
    return 'baz_from_foo(%s)' % x 

class FooBarBaz(object): 
    def foo(self, x): 
     return foo_from_bar(self.bar(x)) 

    def bar(self, x): 
     return bar_from_baz(self.baz(x)) 

    def baz(self, x): 
     return baz_from_foo(self.foo(x)) 

class Derived1(FooBarBaz): 
    def bar(self, x): 
     return 5 

class Derived2(FooBarBaz): 
    def foo(self, x): 
     return 6 

class Derived3(FooBarBaz): 
    def baz(self, x): 
     return 7 

d1 = Derived1() 
d2 = Derived2() 
d3 = Derived3() 

def check(expr): 
    print expr, '->', eval(expr) 

for i,d in enumerate([d1, d2, d3]): 
    print '--- d = Derived%d() ----' % (i+1) 
    check('d.foo(0)') 
    check('d.bar(0)') 
    check('d.baz(0)') 

wyjściowa:

--- d = Derived1() ---- 
d.foo(0) -> foo_from_bar(5) 
d.bar(0) -> 5 
d.baz(0) -> baz_from_foo(foo_from_bar(5)) 
--- d = Derived2() ---- 
d.foo(0) -> 6 
d.bar(0) -> bar_from_baz(baz_from_foo(6)) 
d.baz(0) -> baz_from_foo(6) 
--- d = Derived3() ---- 
d.foo(0) -> foo_from_bar(bar_from_baz(7)) 
d.bar(0) -> bar_from_baz(7) 
d.baz(0) -> 7 
2

dekorator może również rade. Coś wzdłuż tych linii:

def define_missing(cls): 
    has_at_least_one = False 
    if hasattr(cls, 'foo'): 
     if not hasattr(cls, 'bar'): 
      cls.bar = lambda self, x: bar_from_foo(self.foo(x)) 
     if not hasattr(cls, 'baz'): 
      cls.baz = lambda self, x: baz_from_foo(self.foo(x)) 
     has_at_least_one = True 
    if hasattr(cls, 'bar'): 
     if not hasattr(cls, 'foo'): 
      cls.foo = lambda self, x: foo_from_bar(self.bar(x)) 
     if not hasattr(cls, 'baz'): 
      cls.baz = lambda self, x: baz_from_bar(self.bar(x)) 
     has_at_least_one = True 
    if hasattr(cls, 'baz'): 
     if not hasattr(cls, 'bar'): 
      cls.foo = lambda self, x: foo_from_baz(self.baz(x)) 
     if not hasattr(cls, 'baz'): 
      cls.bar = lambda self, x: bar_from_baz(self.baz(x)) 
     has_at_least_one = True 
    if not has_at_least_one: 
     raise TypeError("Class needs to implement at least one of the methods foo, bar, and baz.") 
    return cls 

następnie używać go tak:

@define_missing 
class Derived1(FooBarBaz): 
    def bar(self, x): 
     return 5 

To jest inspirowana przez functools.total_ordering dekoratora.

+0

Jest to nieco niespójne i nie działa tak, jak zostało napisane. Używasz 'foo_from_bar' jako funkcji przekształcającej metodę w inną metodę. Nie chodzi o to, że OP wprowadził 'foo_from_bar' ani o to, co robi dekorator' total_ordering'. Twoim zadaniem powinno być 'cls.foo = lambda self, x: foo_from_bar (self.bar (x))' zamiast tego, aby to podejście działało. –

+0

@Sven: True. Moją intencją było wskazanie, w jaki sposób można wprowadzić taką implementację, ale powinienem wspomnieć, że funkcje te należy na nowo zdefiniować. Edytowałem odpowiedź, aby użyć 'foo_from_bar' itd. Zgodnie z sugestią. Powinno być teraz spójne z tym, w jaki sposób OP je stosował. – freidrichen