2012-06-24 12 views
9

Potrzebuję trochę pomocy w zrozumieniu subtelności protokołu deskryptora w Pythonie, ponieważ odnosi się on konkretnie do zachowania obiektów o rozmiarach staticmethod. Zacznę trywialny przykład, a następnie iteracyjnie go rozwinąć, badając to zachowanie na każdym kroku:Dlaczego dekorowanie klasy łamie protokół deskryptora, zapobiegając w ten sposób zachowaniu obiektów statycznych zgodnie z oczekiwaniami?

class Stub: 
    @staticmethod 
    def do_things(): 
     """Call this like Stub.do_things(), with no arguments or instance.""" 
     print "Doing things!" 

W tym momencie, to zachowuje się zgodnie z oczekiwaniami, ale to, co się tutaj dzieje jest nieco subtelny: Kiedy zadzwoń pod numer Stub.do_things(), nie wywołasz bezpośrednio instrukcji do_things. Zamiast tego, Stub.do_things odnosi się do instancji staticmethod, która zawinęła żądaną funkcję w swój własny protokół deskryptora, tak że faktycznie wywołujesz staticmethod.__get__, która najpierw zwraca żądaną funkcję, a następnie , a następnie zostaje wywołana później.

>>> Stub 
<class __main__.Stub at 0x...> 
>>> Stub.do_things 
<function do_things at 0x...> 
>>> Stub.__dict__['do_things'] 
<staticmethod object at 0x...> 
>>> Stub.do_things() 
Doing things! 

Jak dotąd tak dobrze. Następnie muszę zawinąć klasę w dekoratora, który będzie używany do dostosowywania klasy konkretyzacji - dekorator będzie określić, czy zezwolić na nowe dawałaby lub dostarczyć buforowane instancje:

def deco(cls): 
    def factory(*args, **kwargs): 
     # pretend there is some logic here determining 
     # whether to make a new instance or not 
     return cls(*args, **kwargs) 
    return factory 

@deco 
class Stub: 
    @staticmethod 
    def do_things(): 
     """Call this like Stub.do_things(), with no arguments or instance.""" 
     print "Doing things!" 

Teraz, oczywiście ta część jak jest oczekuje się, że złamie metody statyczne, ponieważ klasa jest teraz ukryta za dekoratorem, tj. Stub nie jest klasą, ale instancją factory, która może wywoływać instancje Stub, gdy ją wywołasz. Rzeczywiście:

>>> Stub 
<function factory at 0x...> 
>>> Stub.do_things 
Traceback (most recent call last): 
    File "<stdin>", line 1, in <module> 
AttributeError: 'function' object has no attribute 'do_things' 
>>> Stub() 
<__main__.Stub instance at 0x...> 
>>> Stub().do_things 
<function do_things at 0x...> 
>>> Stub().do_things() 
Doing things! 

Do tej pory rozumiem, co się tutaj dzieje. Moim celem jest przywrócenie funkcji staticmethods, aby działała tak, jak byś tego oczekiwał, nawet jeśli klasa jest opakowana. Szczęśliwie, Python stdlib zawiera coś, co nazywa się functools, co zapewnia pewne narzędzia tylko w tym celu, tj. Sprawianie, by funkcje zachowywały się bardziej jak inne funkcje, które się zawijają. Więc mogę zmienić dekorator wyglądać następująco:

def deco(cls): 
    @functools.wraps(cls) 
    def factory(*args, **kwargs): 
     # pretend there is some logic here determining 
     # whether to make a new instance or not 
     return cls(*args, **kwargs) 
    return factory 

Teraz wszystko zaczyna się ciekawie:

>>> Stub 
<function Stub at 0x...> 
>>> Stub.do_things 
<staticmethod object at 0x...> 
>>> Stub.do_things() 
Traceback (most recent call last): 
    File "<stdin>", line 1, in <module> 
TypeError: 'staticmethod' object is not callable 
>>> Stub() 
<__main__.Stub instance at 0x...> 
>>> Stub().do_things 
<function do_things at 0x...> 
>>> Stub().do_things() 
Doing things! 

czekać .... co?functools kopiuje staticmethod do funkcji owijania, ale nie można jej wywołać? Dlaczego nie? Za czym tęskniłem?

Grałem z tym przez chwilę i faktycznie wymyśliłem moją własną reimplementację staticmethod, która pozwala jej funkcjonować w tej sytuacji, ale tak naprawdę nie rozumiem, dlaczego było to konieczne lub jeśli jest to nawet najlepsze rozwiązanie tego problemu. Oto pełna przykład:

class staticmethod(object): 
    """Make @staticmethods play nice with decorated classes.""" 

    def __init__(self, func): 
     self.func = func 

    def __call__(self, *args, **kwargs): 
     """Provide the expected behavior inside decorated classes.""" 
     return self.func(*args, **kwargs) 

    def __get__(self, obj, objtype=None): 
     """Re-implement the standard behavior for undecorated classes.""" 
     return self.func 

def deco(cls): 
    @functools.wraps(cls) 
    def factory(*args, **kwargs): 
     # pretend there is some logic here determining 
     # whether to make a new instance or not 
     return cls(*args, **kwargs) 
    return factory 

@deco 
class Stub: 
    @staticmethod 
    def do_things(): 
     """Call this like Stub.do_things(), with no arguments or instance.""" 
     print "Doing things!" 

Rzeczywiście to działa dokładnie tak, jak oczekiwano:

>>> Stub 
<function Stub at 0x...> 
>>> Stub.do_things 
<__main__.staticmethod object at 0x...> 
>>> Stub.do_things() 
Doing things! 
>>> Stub() 
<__main__.Stub instance at 0x...> 
>>> Stub().do_things 
<function do_things at 0x...> 
>>> Stub().do_things() 
Doing things! 

Jakie podejście byłoby podjąć, aby dokonać zachowywać staticmethod jak oczekiwano wewnątrz zdobione klasie? Czy to najlepszy sposób? Dlaczego wbudowany staticmethod nie implementuje na własną rękę __call__, aby to działało bez żadnego zamieszania?

Dzięki.

+1

Dlaczego nie stosować metody klasy? Można go nazwać w ten sam sposób, ale jest mocniejszy i (ja * myślę *) bardziej zgodny z tym i innym metaprogramowaniem. – delnan

+0

Zdarzyło mi się używać 'classmethod's zamiast tego, jednak nie można ich wywoływać (co oznacza, że ​​jest to dokładnie ten sam problem, jaki aktualnie mam), a także dlatego, że w pewnym sensie podoba mi się wyraźne odwoływanie się do' Stub.class_attribute 'wewnątrz' staticmethod' zamiast 'cls.class_attribute' wewnątrz' classmethod'. – robru

Odpowiedz

4

Problem polega na tym, że zmieniasz typ Stub z klasy na funkcję. Jest to dość poważne naruszenie i nie jest wielkim zaskoczeniem, że coś się załamuje.

Przyczyną techniczną, że staticmethod y łamią że functools.wraps prace kopiowania __name__, __doc__ i __module__ itd (źródło: http://hg.python.org/cpython/file/3.2/Lib/functools.py) z owiniętej przykład do owinięcia, podczas aktualizacji owijka na __dict__ z owinięte instancji __dict__. Powinno być oczywiste, dlaczego teraz staticmethod nie działa - jego protokół deskryptora jest wywoływany na funkcji zamiast w klasie, więc rezygnuje z zwracania związanego podpowiedzi i po prostu zwraca swoje nieopłacalne ja.

w.r.t. faktycznie robiąc to, co cię interesuje (jakaś Singleton?), prawdopodobnie chcesz, aby Twój dekorator zwrócił klasę z __new__, która ma wymagane zachowanie. Nie trzeba się martwić o __init__ miano niechciane, jak długo klasy otoki __new__ rzeczywistości nie zwracają wartość typu klasy otoki, raczej instancję zawiniętego Klasa:

def deco(wrapped_cls): 
    @functools.wraps(wrapped_cls) 
    class Wrapper(wrapped_cls): 
     def __new__(cls, *args, **kwargs): 
      ... 
      return wrapped_cls(*args, **kwargs) 
    return Wrapper 

Note rozróżnienie między argumentem do dekoratora (który staje się zamknięty w klasie opakowania) a argumentem cls do .

Należy pamiętać, że używanie klasy functools.wraps w klasie klasy owijającej klasę jest całkowicie OK - po prostu nie w klasie owijającej funkcję!

Można również zmodyfikować owinięte klasę, w takim przypadku nie trzeba functools.wraps:

def deco(wrapped_cls): 
    def __new__(cls, *args, **kwargs) 
     ... 
     return super(wrapped_cls, cls)(*args, **kwargs) 
    wrapped_cls.__new__ = classmethod(__new__) 
    return wrapped_cls 

jednak pamiętać, że ta metoda będzie__init__ skończyć powołując się na istniejących instancji, więc trzeba obejść to (np. zawijając __init__ w celu zwarcia istniejących instancji).

Jako uzupełnienie: może być możliwe wykonanie pracy dekoratora funkcji zawijania klasy w przypadkach, o których wiesz, przy dużym wysiłku, ale nadal będziesz mieć problemy - na przykład isinstance(myObject, Stub) żadna szansa na pracę jako Stub nie jest już !

+0

Nie Singleton, ale memoization. Mój [kod rzeczywistego świata] (https://github.com/robru/gottengeography/blob/ff6bc740c8aa432703a7783cb708c4a986d890ca/gg/common.py#L41) ma znakomity doctest, który wyjaśnia w pełni zachowania i przypadki użycia (odpowiednie są linie od 41 do 124)). Co do twojego rozwiązania, próbowałem tego i to po prostu nie działa. 'Wrapper .__ new__' nie ma nawet wywoływanych i dekorowanych klas wywoływanych przez ich metody' __init__' bez buforowania. – robru

+1

@Robru to nie jest moje rozwiązanie; jest to standardowy wzorzec dla dekoratora klasy Python działającego poprzez dziedziczenie. Możesz także zmodyfikować zawiniętą klasę bez jej podklas; Dam również przykład tego. – ecatmur

+0

Przyjąłem twoją odpowiedź, ponieważ oczywiście włożyłeś w to dużo wysiłku (dziękuję!), Ale to nie zadziała dla mnie niestety, z przyczyn wykraczających poza zakres tego pytania (zobacz link od mój poprzedni komentarz, pakuję funkcje i klasy w ogólny sposób). – robru

2

prawie zrobił to, co ja yould zrobili:

def deco(cls): 
    class factory(cls): 
     def __new__(cls_factory, *args, **kwargs): 
      # pretend there is some logic here determining 
      # whether to make a new instance or not 
      return cls.__new__(*args, **kwargs) 
    return factory 

że powinien to zrobić. Problem może być taki, że __init__ jest wywoływany również w starych instancjach zwracanych przez __new__.

+0

Jednym z problemów, który widzę tutaj, jest to, że muszę używać 'functools.wraps', aby moje testy mogły działać. Sądzę również, że masz rację, jeśli '__init__' jest ponownie uruchamiany, nawet jeśli' __new__' zwraca stare instancje, co jest dokładnie tym, czego potrzebuję uniknąć. – robru

+0

Chociaż udało mi się upvote, ponieważ twoje rozwiązanie, jako podklasa zamiast funkcji opakowanej, pozwala istniejącym 'staticmethod's pracować ze standardowym wbudowanym' staticmethod'. – robru

Powiązane problemy