2013-07-05 5 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 lazy property decorator](http://stackoverflow.com/questions/3012421/python-怠惰なプロパティデコレータ)。 – detly

答えて

11

現在の回答にはいくつかの問題があります。プロパティを持つソリューションでは、追加のクラス属性を指定する必要があり、各ルックアップでこの属性をチェックするオーバーヘッドがあります。 __getattr__の解決策は、最初のアクセスまでこの属性を隠すという問題があります。これはイントロスペクションには悪く、__dir__の回避策は不便です。

2つの提案されたものよりも優れた解決策は、記述子を直接利用することです。 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。

あなたはそれが存在する場合、Pythonは、ディスクリプタを超えるインスタンスの属性を使用して、その時点で、唯一の__get__を実装する独自の記述子を書き込むことができます。

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

あなたが何かをしてい__getattr__アプローチを(希望する場合それを言う)、それはなるだろう:

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) 

以降のアクセスでは、インスタンスと__getattr__bar属性が相談されることはありませんでしょう。

デモ:

>>> 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: 'property'ではなく、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など)でこれを使用する予定の場合は、ロックの安全性を実装することができます。

関連する問題