2013-07-05 40 views
11

foo類有一個欄。直到它被訪問時,條纔會被加載。進一步訪問酒吧應該不會產生開銷。Python - 延遲加載類屬性

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 

是否有可能使用屬性或更好的屬性而不是使用getter方法來做這樣的事情?

的目標是延遲加載不上的所有後續訪問開銷......

+0

你可以用描述自動做到這一點:http://jeetworks.org/node/62 – schlamar

+1

WERKZEUG具有廣泛的一個更好的實現評論:https://github.com/mitsuhiko/werkzeug/blob/10b4b8b6918a83712170fdaabd3ec61cf07f23ff/werkzeug/utils.py#L35 – schlamar

+0

另請參見:[Python懶惰屬性裝飾](http://stackoverflow.com/questions/3012421/python-懶財產裝飾)。 – detly

回答

11

目前的答案有一些問題。具有屬性的解決方案要求您指定一個附加的類屬性,並在每次查找時檢查該屬性的開銷。 __getattr__的解決方案存在這樣的問題,即它在第一次訪問之前隱藏了該屬性。這對內省是不利的,與__dir__的解決方法是不方便的。

比兩個提議的更好的解決方案是直接使用描述符。 werkzeug圖書館已有解決方案werkzeug.utils.cached_property。它有一個簡單的實現,所以你可以直接使用它,而無需WERKZEUG的依賴性:

_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

這個問題不在Web框架(Werkzueg,Django,Bottle,Pyramid等)的範圍之內,這對於線程並不適用。請參閱https://github.com/pydanny/cached-property/issues/6(我們已關閉) – pydanny

8

當然,只要有你的屬性設置實例屬性,其在隨後的訪問返回:

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描述符是一個數據描述符(它實現了__get__,__set____delete__描述符掛鉤),所以即使bar屬性存在於實例中,它也會被調用,最終導致Python忽略該屬性,因此需要測試單獨的ATTRIB在每次訪問時使用ute。如果你喜歡一個__getattr__方法

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

(其中有東西:

你可以,如果它存在,寫自己的描述,只有實現__get__,此時Python用在實例屬性在描述符說吧),那會是:

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) 

的後續訪問將找到實例上的bar屬性和__getattr__不會進行協商。

演示:

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

這增加了每次訪問時額外的「if」的開銷。第一次調用屬性時可以重新定義屬性嗎? –

+0

無論如何你都需要一個標誌,告訴你是否已經實例化該屬性。 –

+1

@whatscanasta:不帶'屬性',因爲Python給數據描述符優先於實例屬性。但是'__getattr__'你可以*(見更新)。 –

1

當然是這樣,嘗試:

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 

請注意,這不是線程安全的。 cPython有GIL,所以這是一個相對的問題,但是如果你打算在一個真正的多線程Python棧(比如Jython)中使用它,你可能想要實現某種形式的鎖安全。