2013-07-05 13 views
11

Klasa foo ma pasek. Pasek nie jest ładowany, dopóki nie zostanie uzyskany dostęp. Dalsze dostępy do paska nie powinny generować żadnych kosztów ogólnych.Python - Leniwe ładowanie atrybutów klasowych

class Foo(object): 

    def get_bar(self): 
     print "initializing" 
     self.bar = "12345" 
     self.get_bar = self._get_bar 
     return self.bar 

    def _get_bar(self): 
     print "accessing" 
     return self.bar 

Czy można zrobić coś takiego przy użyciu właściwości lub, jeszcze lepiej, atrybutów, zamiast korzystać z metody gettera?

Celem jest leniwy obciążenia bez narzutu na wszystkich kolejnych dostępów ...

+0

można to zrobić automatycznie z deskryptorów: http://jeetworks.org/node/62 – schlamar

+1

Werkzeug ma lepszego wdrażania z rozległymi komentarze: https://github.com/mitsuhiko/werkzeug/blob/10b4b8b6918a83712170fdaabd3ec61cf07f23ff/werkzeug/utils.py#L35 – schlamar

+0

Zobacz także: [Python lazy property decorator] (http://stackoverflow.com/questions/3012421/python- leniwe-własności-dekorator). – detly

Odpowiedz

11

Występują pewne problemy z bieżącymi odpowiedziami. Rozwiązanie z właściwością wymaga określenia dodatkowego atrybutu klasy i wiąże się z koniecznością sprawdzenia tego atrybutu przy każdym wyszukiwaniu. Rozwiązanie z __getattr__ ma problem z ukrywaniem tego atrybutu do pierwszego dostępu. To jest złe dla introspekcji i obejście z __dir__ jest niewygodne.

Lepszym rozwiązaniem niż dwa proponowane jest wykorzystanie bezpośrednio deskryptorów. Biblioteka werkzeug ma już rozwiązanie jako werkzeug.utils.cached_property. Posiada prostą implementację, dzięki czemu można go używać bezpośrednio, bez konieczności Werkzeug jako zależność:

_missing = object() 

class cached_property(object): 
    """A decorator that converts a function into a lazy property. The 
    function wrapped is called the first time to retrieve the result 
    and then that calculated result is used the next time you access 
    the value:: 

     class Foo(object): 

      @cached_property 
      def foo(self): 
       # calculate something important here 
       return 42 

    The class has to have a `__dict__` in order for this property to 
    work. 
    """ 

    # implementation detail: this property is implemented as non-data 
    # descriptor. non-data descriptors are only invoked if there is 
    # no entry with the same name in the instance's __dict__. 
    # this allows us to completely get rid of the access function call 
    # overhead. If one choses to invoke __get__ by hand the property 
    # will still work as expected because the lookup logic is replicated 
    # in __get__ for manual invocation. 

    def __init__(self, func, name=None, doc=None): 
     self.__name__ = name or func.__name__ 
     self.__module__ = func.__module__ 
     self.__doc__ = doc or func.__doc__ 
     self.func = func 

    def __get__(self, obj, type=None): 
     if obj is None: 
      return self 
     value = obj.__dict__.get(self.__name__, _missing) 
     if value is _missing: 
      value = self.func(obj) 
      obj.__dict__[self.__name__] = value 
     return value 
+4

Problem z tym jest poza zakresem ramy internetowej (Werkzueg, Django, Bottle, Pyramid, et al), to nie działa dobrze z wątkami. Zobacz https://github.com/pydanny/cached-property/issues/6 (które zamknęliśmy) – pydanny

8

Jasne, tylko że nieruchomość ustawiony atrybut instancją, która jest zwracana na późniejsze dostępu:

class Foo(object): 
    _cached_bar = None 

    @property 
    def bar(self): 
     if not self._cached_bar: 
      self._cached_bar = self._get_expensive_bar_expression() 
     return self._cached_bar 

property deskryptora jest deskryptorem danych (implementuje haki deskryptora), więc będzie wywoływany, nawet jeśli na instancji istnieje atrybut bar, z wynikiem końcowym, że Python zignoruje ten atrybut, stąd potrzeba przetestowania osobnego atrybutu attrib przy każdym dostępie.

Możesz napisać własny deskryptor że tylko implementuje __get__, w którym momencie Python używa atrybutu na przykład na deskryptorze jeśli istnieje:

class CachedProperty(object): 
    def __init__(self, func, name=None): 
     self.func = func 
     self.name = name if name is not None else func.__name__ 
     self.__doc__ = func.__doc__ 

    def __get__(self, instance, class_): 
     if instance is None: 
      return self 
     res = self.func(instance) 
     setattr(instance, self.name, res) 
     return res 

class Foo(object): 
    @CachedProperty 
    def bar(self): 
     return self._get_expensive_bar_expression() 

Jeśli wolisz podejście __getattr__ (który ma coś do mówią o tym), że byłoby:

class Foo(object): 
    def __getattr__(self, name): 
     if name == 'bar': 
      bar = self.bar = self._get_expensive_bar_expression() 
      return bar 
     return super(Foo, self).__getattr__(name) 

Późniejsza dostęp znajdzie atrybut bar na przykład i __getattr__ nie będzie konsultowany.

Demo:

>>> class FooExpensive(object): 
...  def _get_expensive_bar_expression(self): 
...   print 'Doing something expensive' 
...   return 'Spam ham & eggs' 
... 
>>> class FooProperty(FooExpensive): 
...  _cached_bar = None 
...  @property 
...  def bar(self): 
...   if not self._cached_bar: 
...    self._cached_bar = self._get_expensive_bar_expression() 
...   return self._cached_bar 
... 
>>> f = FooProperty() 
>>> f.bar 
Doing something expensive 
'Spam ham & eggs' 
>>> f.bar 
'Spam ham & eggs' 
>>> vars(f) 
{'_cached_bar': 'Spam ham & eggs'} 
>>> class FooDescriptor(FooExpensive): 
...  bar = CachedProperty(FooExpensive._get_expensive_bar_expression, 'bar') 
... 
>>> f = FooDescriptor() 
>>> f.bar 
Doing something expensive 
'Spam ham & eggs' 
>>> f.bar 
'Spam ham & eggs' 
>>> vars(f) 
{'bar': 'Spam ham & eggs'} 

>>> class FooGetAttr(FooExpensive): 
...  def __getattr__(self, name): 
...   if name == 'bar': 
...    bar = self.bar = self._get_expensive_bar_expression() 
...    return bar 
...   return super(Foo, self).__getatt__(name) 
... 
>>> f = FooGetAttr() 
>>> f.bar 
Doing something expensive 
'Spam ham & eggs' 
>>> f.bar 
'Spam ham & eggs' 
>>> vars(f) 
{'bar': 'Spam ham & eggs'} 
+0

Powoduje to dodatkowy dodatkowy "if" przy każdym dostępie. Czy możliwe jest ponowne zdefiniowanie nieruchomości przy pierwszym jej wywołaniu? –

+0

W każdym razie potrzebowałbyś flagi, coś, co powie ci, czy już stworzyłeś tę właściwość, czy nie. –

+1

@whatscanasta: Nie z 'właściwością', ponieważ Python nadaje priorytet deskryptorów danych nad atrybutami instancji. Ale z '__getattr__' możesz * możesz * (patrz aktualizacja). –

1

Jasne jest, spróbuj:

class Foo(object): 
    def __init__(self): 
     self._bar = None # Initial value 

    @property 
    def bar(self): 
     if self._bar is None: 
      self._bar = HeavyObject() 
     return self._bar 

zauważyć, że nie jest bezpieczny wątku. cPython ma GIL, więc jest to kwestia względna, ale jeśli planujesz użyć tego w prawdziwym stosie wielowątkowym w języku Python (powiedzmy Jython), możesz chcieć zaimplementować jakąś formę bezpieczeństwa blokady.

Powiązane problemy