2013-07-05 17 views
11

La classe foo ha una barra. La barra non viene caricata fino a quando non vi si accede. Ulteriori accessi alla barra non dovrebbero comportare spese generali.Python - Caricamento lento degli attributi della classe

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 

E 'possibile fare qualcosa di simile utilizzando le proprietà o, meglio ancora, gli attributi, invece di utilizzare un metodo getter?

L'obiettivo è quello di caricare pigro senza spese generali su tutti gli accessi successivi ...

+0

Potete farlo automaticamente con descrittori: http://jeetworks.org/node/62 – schlamar

+1

Werkzeug ha una migliore attuazione con ampia commenti: https://github.com/mitsuhiko/werkzeug/blob/10b4b8b6918a83712170fdaabd3ec61cf07f23ff/werkzeug/utils.py#L35 – schlamar

+0

Vedi anche: [Python lazy property decorator] (http://stackoverflow.com/questions/3012421/python- lazy-proprietà-decorator). – detly

risposta

11

Ci sono alcuni problemi con le risposte attuali. La soluzione con una proprietà richiede di specificare un attributo di classe aggiuntivo e ha il sovraccarico di controllare questo attributo in ogni ricerca. La soluzione con __getattr__ ha il problema che nasconde questo attributo fino al primo accesso. Ciò è negativo per l'introspezione e una soluzione alternativa con __dir__ è scomoda.

Una soluzione migliore rispetto a quelle proposte è l'utilizzo diretto dei descrittori. La libreria werkzeug ha già una soluzione come werkzeug.utils.cached_property. Ha una semplice implementazione in modo da poter utilizzare direttamente senza dover Werkzeug come dipendenza:

_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

Il problema con questo è al di fuori dell'ambito di un framework web (Werkzueg, Django, Bottle, Pyramid, et al), questo non funziona bene con i thread. Vedi https://github.com/pydanny/cached-property/issues/6 (che abbiamo chiuso) – pydanny

8

Certo, basta avere la vostra proprietà ubicata attributo un'istanza che viene restituito il successivo accesso:

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 

Il descrittore property è un descrittore di dati (implementa gli hook del descrittore __get__, __set__ e __delete__), quindi verrà invocato anche se esiste un attributo bar nell'istanza, con il risultato finale che Python ignora tale attributo, quindi la necessità di testare per un separato attrib ute su ogni accesso.

È possibile scrivere il proprio descrittore che implementa solo __get__, a quel punto Python utilizza un attributo sull'istanza sopra il descrittore se esiste:

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

Se si preferisce un approccio __getattr__ (che ha qualcosa a che dicono per esso), che sarebbe:

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) 

accesso successivo sarà trovare l'attributo bar sull'istanza e non verrà consultato __getattr__.

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

Questo aggiunge un sovraccarico di un extra "se" su ogni accesso. È possibile ridefinire la proprietà la prima volta che viene chiamata? –

+0

Avresti comunque bisogno di una bandiera da qualche parte, qualcosa che ti dice se hai già istanziato la proprietà o meno. –

+1

@whatscanasta: non con una 'proprietà', perché Python attribuisce la priorità ai descrittori di dati sugli attributi di istanza. Ma con '__getattr__' tu * puoi * (vedi aggiornamento). –

1

Certo che lo è, provare:

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 

Si noti che questo non è thread-safe. cPython ha GIL, quindi è un problema relativo, ma se prevedi di usarlo in un vero stack Python multithread (per esempio, Jython), potresti voler implementare una qualche forma di lock security.

Problemi correlati