Python hafızaya alma / ertelenmiş arama özelliği dekoratörü


109

Son zamanlarda, örnek niteliklerinin bir veritabanında depolanan değerleri yansıttığı birçok sınıf içeren mevcut bir kod tabanından geçtim. Veritabanı aramalarının ertelenmesi için bu özniteliklerin çoğunu yeniden düzenledim. yapıcıda başlatılmamalıdır, ancak yalnızca ilk okunduğunda. Bu öznitelikler, örneğin kullanım ömrü boyunca değişmez, ancak ilk kez hesaplamak için gerçek bir darboğazdır ve yalnızca özel durumlar için gerçekten erişilir. Bu nedenle, veritabanından alındıktan sonra da önbelleğe alınabilir (bu nedenle bu , girdinin basitçe "giriş yok" olduğu hafızaya alma tanımına uyar ).

Kendimi, çeşitli sınıflarda çeşitli öznitelikler için aşağıdaki kod parçacığını tekrar tekrar yazarken buluyorum:

class testA(object):

  def __init__(self):
    self._a = None
    self._b = None

  @property
  def a(self):
    if self._a is None:
      # Calculate the attribute now
      self._a = 7
    return self._a

  @property
  def b(self):
    #etc

Bunu zaten Python'da yapacak, farkında olmadığım bir dekoratör var mı? Ya da, bunu yapan bir dekoratörü tanımlamanın oldukça basit bir yolu var mı?

Python 2.5 altında çalışıyorum, ancak 2.6 cevapları önemli ölçüde farklıysa yine de ilginç olabilir.

Not

Bu soru, Python bunun için birçok hazır dekoratör eklemeden önce sorulmuştu. Sadece terminolojiyi düzeltmek için güncelledim.


Python 2.7 kullanıyorum ve bunun için hazır dekoratörler hakkında hiçbir şey görmüyorum. Soruda adı geçen hazır dekoratörlere bir link verebilir misiniz?
Bamcclur

@Bamcclur üzgünüm, onları detaylandıran başka yorumlar vardı, neden silindiklerinden emin değilim. Şu an bulabilecek tek kişi bir Python 3 biridir: functools.lru_cache().
detly


@guyarad Bu yorumu şimdiye kadar görmedim. Bu harika bir kütüphane! Bunu bir cevap olarak gönder ki ben de ona oy verebileyim.
detly

Yanıtlar:


12

Her tür harika hizmet için bolton kullanıyorum .

Bu kitaplığın bir parçası olarak önbelleğe alınmış özelliğe sahipsiniz :

from boltons.cacheutils import cachedproperty

class Foo(object):
    def __init__(self):
        self.value = 4

    @cachedproperty
    def cached_prop(self):
        self.value += 1
        return self.value


f = Foo()
print(f.value)  # initial value
print(f.cached_prop)  # cached property is calculated
f.value = 1
print(f.cached_prop)  # same value for the cached property - it isn't calculated again
print(f.value)  # the backing value is different (it's essentially unrelated value)

124

İşte tembel bir özellik dekoratörünün örnek bir uygulaması:

import functools

def lazyprop(fn):
    attr_name = '_lazy_' + fn.__name__

    @property
    @functools.wraps(fn)
    def _lazyprop(self):
        if not hasattr(self, attr_name):
            setattr(self, attr_name, fn(self))
        return getattr(self, attr_name)

    return _lazyprop


class Test(object):

    @lazyprop
    def a(self):
        print 'generating "a"'
        return range(5)

Etkileşimli oturum:

>>> t = Test()
>>> t.__dict__
{}
>>> t.a
generating "a"
[0, 1, 2, 3, 4]
>>> t.__dict__
{'_lazy_a': [0, 1, 2, 3, 4]}
>>> t.a
[0, 1, 2, 3, 4]

1
Birisi iç işlev için uygun bir isim önerebilir mi? Sabahları isimlendirmede çok kötüyüm ...
Mike Boers

2
Genelde iç işlevi dış işlevle aynı şekilde, önünde bir alt çizgi ile adlandırırım. Öyleyse "_lazyprop" - 8.
sezonun

1
Bu harika çalışıyor :) Neden böyle iç içe geçmiş bir işlevde dekoratör kullanmak hiç aklıma gelmedi bilmiyorum.
detly

4
veri tanımlayıcı olmayan protokol göz önüne alındığında, bu, aşağıdaki cevaptan çok daha yavaş ve daha az zariftir__get__
Ronny

1
İpucu: @wraps(fn)@propertywrapsfunctools
Doküman

111

Bunu kendim için yazdım ... Gerçek tek seferlik hesaplanan tembel mülkler için kullanılmak üzere . Bunu seviyorum çünkü nesnelere fazladan nitelikler yapıştırmaktan kaçınıyor ve bir kez etkinleştirildiğinde nitelik mevcudiyetini vb. Kontrol etmek için zaman kaybetmiyor:

import functools

class lazy_property(object):
    '''
    meant to be used for lazy evaluation of an object attribute.
    property should represent non-mutable data, as it replaces itself.
    '''

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

        # copy the getter function's docstring and other attributes
        functools.update_wrapper(self, fget)

    def __get__(self, obj, cls):
        if obj is None:
            return self

        value = self.fget(obj)
        setattr(obj, self.fget.__name__, value)
        return value


class Test(object):

    @lazy_property
    def results(self):
        calcs = 1  # Do a lot of calculation here
        return calcs

Not: lazy_propertySınıf, veri olmayan bir tanımlayıcıdır , yani salt okunurdur. Bir __set__yöntem eklemek, onun doğru çalışmasını engelleyecektir.


9
Bunu anlamak biraz zaman aldı ama kesinlikle çarpıcı bir cevap. Fonksiyonun kendisinin hesapladığı değerle nasıl değiştirildiğini seviyorum.
Paul Etherton

2
Gelecek kuşaklar için: bunun diğer versiyonları, o zamandan beri diğer cevaplarda önerilmiştir (ref 1 ve 2 ) . Görünüşe göre bu Python web çerçevelerinde popüler bir yöntem (türevler Pyramid ve Werkzeug'da mevcuttur).
André Caron

1
Werkzeug'un werkzeug.utils.cached_property'ye sahip olduğunu belirttiğiniz için teşekkür ederiz: werkzeug.pocoo.org/docs/utils/#werkzeug.utils.cached_property
divieira

3
Bu yöntemi seçilen cevaba göre 7.6 kat daha hızlı buldum. (2,45 µs / 322 ns) ipython not defterine bakın
Dave Butler

1
Not: Bu atama engellemez için fgetyolu @propertyyok. Değişmezliği / idempotence sağlamak için __set__(), yükselten bir yöntem eklemeniz gerekir AttributeError('can\'t set attribute')(veya size uyan istisna / mesaj ne olursa olsun, ama bu propertyyükseltir). Bunun nedeni, ne yazık ki, bir mikro bölgesinin bir kısmını bir performans etkisi ile birlikte __get__(), her bir erişim yerine, çekme fget değerine çağrılır dict , ikinci ve daha sonraki erişim. Benim görüşüme göre, kullanım durumlarım için anahtar olan değişmezliği / idempotansı korumaya değer, ancak YMMV.
scanny

4

Burada yer, isteğe bağlı bir zaman aşımı argüman alır bir çağrılabilir var __call__ayrıca üzerinde kopyalayabilirsiniz __name__, __doc__, __module__fonk en ad alanından:

import time

class Lazyproperty(object):

    def __init__(self, timeout=None):
        self.timeout = timeout
        self._cache = {}

    def __call__(self, func):
        self.func = func
        return self

    def __get__(self, obj, objcls):
        if obj not in self._cache or \
          (self.timeout and time.time() - self._cache[key][1] > self.timeout):
            self._cache[obj] = (self.func(obj), time.time())
        return self._cache[obj]

örn:

class Foo(object):

    @Lazyproperty(10)
    def bar(self):
        print('calculating')
        return 'bar'

>>> x = Foo()
>>> print(x.bar)
calculating
bar
>>> print(x.bar)
bar
...(waiting 10 seconds)...
>>> print(x.bar)
calculating
bar

3

propertybir sınıftır. Kesin olması gereken bir tanımlayıcı . Basitçe ondan türetin ve istenen davranışı uygulayın.

class lazyproperty(property):
   ....

class testA(object):
   ....
  a = lazyproperty('_a')
  b = lazyproperty('_b')

3

Ne gerçekten istiyorum olduğunu reify(! Kaynak bağlantılı) Piramit dekoratör:

Sınıf yöntemi dekoratörü olarak kullanın. Neredeyse tıpkı Python @propertydekoratörü gibi çalışır , ancak süslediği yöntemin sonucunu ilk çağrıdan sonra örnek dikteye koyar ve süslediği işlevi etkili bir şekilde bir örnek değişkeni ile değiştirir. Python dilinde veri olmayan bir tanımlayıcıdır. Aşağıda bir örnek ve kullanımı verilmiştir:

>>> from pyramid.decorator import reify

>>> class Foo(object):
...     @reify
...     def jammy(self):
...         print('jammy called')
...         return 1

>>> f = Foo()
>>> v = f.jammy
jammy called
>>> print(v)
1
>>> f.jammy
1
>>> # jammy func not called the second time; it replaced itself with 1
>>> # Note: reassignment is possible
>>> f.jammy = 2
>>> f.jammy
2

1
Güzel olan, tam olarak ihtiyacım olanı yapıyor ... Piramit bir dekoratör için büyük bir bağımlılık olsa da:)
2016

@detly Dekoratör uygulaması basittir ve bunu kendiniz uygulayabilirsiniz, pyramidbağımlılığa gerek yoktur .
Peter Wood

Bağlantıda "kaynak bağlantılı" yazıyor: D
Antti Haapala

@AnttiHaapala Fark ettim, ancak bağlantıyı takip etmeyenler için uygulamanın basit olduğunu vurgulayacağımı düşündüm.
Peter Wood

1

Şimdiye kadar hem söz konusu hem de yanıtlarda bir dizi terim ve / veya kavram karmaşası var.

Tembel değerlendirme, bir şeyin bir değere ihtiyaç duyulduğunda mümkün olan en son anda çalışma zamanında değerlendirildiği anlamına gelir. Standart @propertydekoratör tam da bunu yapar. (*) Süslü işlev yalnızca ve bu özelliğin değerine her ihtiyacınız olduğunda değerlendirilir. (tembel değerlendirme hakkındaki wikipedia makalesine bakın)

(*) Aslında gerçek bir tembel değerlendirme (haskell ile karşılaştırın) python'da elde etmek çok zordur (ve deyimsel olmaktan uzak bir kodla sonuçlanır).

Hatırlatma, soruyu soranın aradığı şey için doğru terimdir. Dönüş değeri değerlendirmesi için yan etkilere bağlı olmayan saf işlevler güvenli bir şekilde hafızaya alınabilir ve aslında işlev araçlarında bir dekoratör vardır, @functools.lru_cachebu nedenle özel bir davranışa ihtiyacınız olmadıkça kendi dekoratörlerinizi yazmaya gerek yoktur.


"Tembel" terimini kullandım çünkü orijinal uygulamada üye, nesne başlatılırken bir DB'den hesaplandı / alındı ​​ve bu hesaplamayı, özellik gerçekten bir şablonda kullanılana kadar ertelemek istiyorum. Bu bana tembellik tanımına uyuyor gibiydi. Sorum zaten bir çözüm olduğunu varsaydığından @property, "tembel" kullanımının bu noktada pek bir anlam ifade etmediğine katılıyorum . (Ayrıca hafızayı, önbelleğe alınmış çıktıların girdilerinin bir haritası olarak düşündüm ve bu özelliklerin yalnızca bir girdisi, hiçbir şeyi olmadığı için, bir harita gerekenden daha karmaşık görünüyordu.)
2016

İnsanların "kutudan çıkar çıkmaz" olarak önerdiği tüm dekoratörlerin ben de bunu sorduğumda mevcut olmadığını unutmayın.
detly

Jason'a katılıyorum, bu soru önbelleğe alma / not alma ile ilgili bir değerlendirme değil.
poindexter

@poindexter - Önbelleğe alma bunu tam olarak kapsamaz; değere nesne başlatma zamanında bakmayı ve değeri yukarı bakıp önbelleğe almaktan ayırt etmez (buradaki temel özellik budur). Ona ne demeliyim? "İlk kullanımdan sonra önbellek" dekoratörü?
detly

@detly Memoize. Memoize demelisin. en.wikipedia.org/wiki/Memoization
poindexter

0

Python yerel özelliğinden bir sınıf oluşturarak bunu güzel ve kolay bir şekilde yapabilirsiniz:

class cached_property(property):
    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 __set__(self, obj, value):
        obj.__dict__[self.__name__] = value

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

Bu özellik sınıfını normal sınıf özelliği gibi kullanabiliriz (Aynı zamanda görebileceğiniz gibi öğe atamasını da destekler)

class SampleClass():
    @cached_property
    def cached_property(self):
        print('I am calculating value')
        return 'My calculated value'


c = SampleClass()
print(c.cached_property)
print(c.cached_property)
c.cached_property = 2
print(c.cached_property)
print(c.cached_property)

Değer sadece ilk kez hesaplandı ve bundan sonra tasarruf ettiğimiz değeri kullandık

Çıktı:

I am calculating value
My calculated value
My calculated value
2
2
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.