Python การบันทึก / การค้นหาคุณสมบัติรอการตัดบัญชี


109

เมื่อเร็ว ๆ นี้ฉันได้อ่านฐานรหัสที่มีอยู่ซึ่งมีคลาสมากมายที่แอตทริบิวต์ของอินสแตนซ์แสดงถึงค่าที่เก็บไว้ในฐานข้อมูล ฉันได้ปรับโครงสร้างใหม่จำนวนมากเพื่อให้การค้นหาฐานข้อมูลถูกเลื่อนออกไปเช่น ไม่ได้เริ่มต้นในตัวสร้าง แต่เมื่ออ่านครั้งแรกเท่านั้น แอตทริบิวต์เหล่านี้จะไม่เปลี่ยนแปลงตลอดอายุการใช้งานของอินสแตนซ์ แต่เป็นคอขวดที่แท้จริงในการคำนวณครั้งแรกและเข้าถึงได้เฉพาะกรณีพิเศษเท่านั้น ดังนั้นจึงสามารถแคชได้หลังจากที่ดึงข้อมูลจากฐานข้อมูลแล้ว (ดังนั้นจึงเหมาะกับคำจำกัดความของบันทึกช่วยจำโดยที่อินพุตเป็นเพียง "ไม่มีอินพุต")

ฉันพบว่าตัวเองกำลังพิมพ์ข้อมูลโค้ดต่อไปนี้ซ้ำแล้วซ้ำอีกสำหรับแอตทริบิวต์ต่างๆในคลาสต่างๆ:

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

มีมัณฑนากรที่ทำสิ่งนี้แล้วใน Python ที่ฉันไม่รู้หรือไม่? หรือมีวิธีง่ายๆในการกำหนดมัณฑนากรที่ทำสิ่งนี้หรือไม่?

ฉันทำงานภายใต้ Python 2.5 แต่ 2.6 คำตอบอาจยังน่าสนใจหากมีความแตกต่างอย่างมีนัยสำคัญ

บันทึก

คำถามนี้ถูกถามก่อนที่ Python จะรวมมัณฑนากรสำเร็จรูปไว้มากมายสำหรับสิ่งนี้ ฉันได้อัปเดตเพื่อแก้ไขคำศัพท์เท่านั้น


ฉันใช้ Python 2.7 และไม่เห็นอะไรเกี่ยวกับมัณฑนากรสำเร็จรูปสำหรับสิ่งนี้ คุณสามารถให้ลิงค์ไปยังมัณฑนากรสำเร็จรูปที่กล่าวถึงในคำถามได้หรือไม่?
Bamcclur

@Bamcclur ขออภัยเคยมีความคิดเห็นอื่น ๆ ที่ให้รายละเอียดเกี่ยวกับพวกเขาไม่แน่ใจว่าทำไมพวกเขาถึงถูกลบ เพียงคนเดียวที่ฉันสามารถหาได้ในขณะนี้เป็นงูหลาม 3 functools.lru_cache()ที่หนึ่ง:
ย้อนกลับ

ไม่แน่ใจว่ามีในตัว (อย่างน้อย Python 2.7) แต่มีคุณสมบัติแคช
Guyarad

@guyarad ฉันไม่เห็นความคิดเห็นนี้จนถึงตอนนี้ นั่นคือห้องสมุดที่ยอดเยี่ยม! โพสต์เป็นคำตอบเพื่อให้ฉันโหวตให้คะแนน
ย้อนหลัง

คำตอบ:


12

สำหรับทุกประเภทของสาธารณูปโภคที่ดีผมใช้Boltons

ในฐานะที่เป็นส่วนหนึ่งของไลบรารีนั้นคุณมีคุณสมบัติแคช :

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

นี่คือตัวอย่างการใช้งานตัวตกแต่งอสังหาริมทรัพย์ที่ขี้เกียจ:

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)

เซสชันแบบโต้ตอบ:

>>> 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
มีใครแนะนำชื่อที่เหมาะสมสำหรับฟังก์ชันภายในได้ไหม ฉันแย่มากที่ตั้งชื่อสิ่งต่างๆในตอนเช้า ...
Mike Boers

2
ฉันมักจะตั้งชื่อฟังก์ชันภายในเหมือนกับฟังก์ชันด้านนอกโดยมีขีดล่างนำหน้า ดังนั้น "_lazyprop" - เป็นไปตามปรัชญา "ใช้ภายในเท่านั้น" ของ pep 8.
เวลา

1
มันใช้งานได้ดี :) ฉันไม่รู้ว่าทำไมฉันถึงไม่เคยใช้มัณฑนากรกับฟังก์ชันซ้อนกันแบบนั้นด้วย
ย้อนกลับ

4
ด้วยโปรโตคอลตัวบอกข้อมูลที่ไม่ใช่ข้อมูลโปรโตคอลนี้ช้ากว่ามากและไม่หรูหรากว่าคำตอบด้านล่างโดยใช้__get__
Ronny

1
เคล็ดลับ: ใส่@wraps(fn)ด้านล่าง@propertyเพื่อไม่ให้สตริงเอกสารของคุณหลวมเป็นต้น ( wrapsมาจากfunctools)
letmaik

111

ฉันเขียนสิ่งนี้เพื่อตัวเอง ... เพื่อใช้สำหรับคุณสมบัติขี้เกียจคำนวณครั้งเดียวที่แท้จริง ฉันชอบมันเพราะมันหลีกเลี่ยงการติดแอตทริบิวต์พิเศษบนวัตถุและเมื่อเปิดใช้งานแล้วไม่ต้องเสียเวลาตรวจสอบการมีอยู่ของแอตทริบิวต์ ฯลฯ :

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

หมายเหตุ: lazy_propertyคลาสนี้เป็นตัวบอกข้อมูลที่ไม่ใช่ข้อมูลซึ่งหมายความว่าเป็นแบบอ่านอย่างเดียว การเพิ่ม__set__วิธีการจะทำให้ไม่สามารถทำงานได้อย่างถูกต้อง


9
ใช้เวลาสักครู่ในการทำความเข้าใจ แต่เป็นคำตอบที่น่าทึ่งอย่างยิ่ง ฉันชอบวิธีที่ฟังก์ชันถูกแทนที่ด้วยค่าที่คำนวณ
Paul Etherton

2
สำหรับลูกหลาน: มีการเสนอเวอร์ชันอื่น ๆ ในคำตอบอื่น ๆ ตั้งแต่ (อ้างอิง1และ2 ) ดูเหมือนจะเป็นที่นิยมในเว็บเฟรมเวิร์ก Python (มีอนุพันธ์อยู่ใน Pyramid และ Werkzeug)
André Caron

1
ขอขอบคุณที่แจ้งให้ทราบว่า Werkzeug มี werkzeug.utils.cached_property: werkzeug.pocoo.org/docs/utils/#werkzeug.utils.cached_property
divieira

3
ฉันพบว่าวิธีนี้เร็วกว่าคำตอบที่เลือก 7.6 เท่า (2.45 µs / 322 ns) ดูสมุดบันทึก ipython
Dave Butler

1
หมายเหตุ: นี้ไม่ได้ป้องกันการได้รับมอบหมายเพื่อfgetวิธีการที่@propertyไม่ เพื่อให้แน่ใจว่าไม่เปลี่ยนรูป / idempotence คุณต้องเพิ่ม__set__()วิธีการที่เพิ่มขึ้นAttributeError('can\'t set attribute')(หรือข้อยกเว้น / ข้อความใดก็ตามที่เหมาะกับคุณ แต่นี่คือสิ่งที่propertyเพิ่มขึ้น) สิ่งนี้มาพร้อมกับผลกระทบด้านประสิทธิภาพเพียงเศษเสี้ยวของไมโครวินาทีเนื่องจาก__get__()จะถูกเรียกใช้ในการเข้าถึงแต่ละครั้งแทนที่จะดึงค่า fget จากdictในการเข้าถึงครั้งที่สองและครั้งต่อ ๆ ไป ในความคิดของฉันมันคุ้มค่าที่จะรักษาความไม่เปลี่ยนรูป / idempotence ซึ่งเป็นกุญแจสำคัญสำหรับกรณีการใช้งานของฉัน แต่ YMMV
scanny

4

นี่เป็น callable ที่เกิดการโต้เถียงหมดเวลาตัวเลือกใน__call__คุณยังสามารถคัดลอกเหนือ__name__, __doc__, __module__จาก namespace func ของ:

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]

เช่น:

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

propertyเป็นชั้นเรียน ตัวบ่งชี้ที่ถูกต้อง เพียงแค่ได้มาจากมันและใช้พฤติกรรมที่ต้องการ

class lazyproperty(property):
   ....

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

3

สิ่งที่คุณต้องการจริงๆคือมัณฑนากรreify(ลิงก์แหล่งที่มา!)จาก Pyramid:

ใช้เป็นตัวตกแต่งเมธอดคลาส มันทำงานเกือบจะเหมือนกับ Python@propertyมัณฑนากรแต่จะทำให้ผลลัพธ์ของวิธีการที่มันตกแต่งเป็นอินสแตนซ์ดิคหลังการเรียกครั้งแรกโดยแทนที่ฟังก์ชันที่มันตกแต่งด้วยตัวแปรอินสแตนซ์ได้อย่างมีประสิทธิภาพ ใน Python parlance ไม่ใช่ data descriptor ต่อไปนี้เป็นตัวอย่างและการใช้งาน:

>>> 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
ดีมากทำในสิ่งที่ฉันต้องการ ... แม้ว่าพีระมิดอาจเป็นที่พึ่งพาที่ยิ่งใหญ่สำหรับมัณฑนากรคนหนึ่ง:)
ก็ตาม

@ รายละเอียดการใช้งานมัณฑนากรเป็นเรื่องง่ายและคุณสามารถใช้งานได้ด้วยตัวเองไม่จำเป็นต้องpyramidพึ่งพา
Peter Wood

ดังนั้นลิงก์จึงระบุว่า "source linked": D
Antti Haapala

@AnttiHaapala ฉันสังเกตเห็น แต่คิดว่าฉันเน้นว่ามันง่ายที่จะใช้สำหรับผู้ที่ไม่ไปตามลิงค์
Peter Wood

1

จนถึงขณะนี้มีข้อตกลงและ / หรือความสับสนทั้งในคำถามและคำตอบ

การประเมินแบบขี้เกียจหมายความว่ามีการประเมินบางสิ่งในรันไทม์ในช่วงเวลาสุดท้ายที่เป็นไปได้เมื่อต้องการค่า มาตรฐาน@propertyมัณฑนากรทำเพียงแค่นั้น(*) ฟังก์ชันตกแต่งจะได้รับการประเมินเท่านั้นและทุกครั้งที่คุณต้องการมูลค่าของทรัพย์สินนั้น (ดูบทความวิกิพีเดียเกี่ยวกับการประเมินแบบขี้เกียจ)

(*) ที่จริงแล้วการประเมินแบบขี้เกียจจริง ๆ (เปรียบเทียบเช่น haskell) เป็นเรื่องยากมากที่จะบรรลุใน python (และส่งผลให้เกิดรหัสที่ห่างไกลจากสำนวน)

การท่องจำเป็นคำที่ถูกต้องสำหรับสิ่งที่ผู้ถามกำลังมองหา ฟังก์ชั่นบริสุทธิ์ที่ไม่ขึ้นอยู่กับผลข้างเคียงสำหรับการประเมินมูลค่าผลตอบแทนสามารถบันทึกได้อย่างปลอดภัยและมีมัณฑนากรในฟังก์ชั่นเครื่องมือ @functools.lru_cacheดังนั้นจึงไม่จำเป็นต้องเขียนมัณฑนากรของตัวเองเว้นแต่คุณจะต้องการพฤติกรรมพิเศษ


ฉันใช้คำว่า "lazy" เนื่องจากในการนำไปใช้งานเดิมสมาชิกถูกคำนวณ / ดึงข้อมูลจากฐานข้อมูลในช่วงเวลาของการเริ่มต้นอ็อบเจ็กต์และฉันต้องการเลื่อนการคำนวณนั้นออกไปจนกว่าคุณสมบัติจะถูกใช้ในเทมเพลตจริงๆ สิ่งนี้ดูเหมือนสำหรับฉันจะตรงกับคำจำกัดความของความเกียจคร้าน ฉันยอมรับว่าเนื่องจากคำถามของฉันมีการใช้วิธีแก้ปัญหาอยู่แล้ว@property"ขี้เกียจ" จึงไม่สมเหตุสมผลในตอนนั้น (ฉันยังคิดว่าการช่วยจำเป็นแผนที่ของอินพุตไปยังเอาต์พุตที่แคชไว้และเนื่องจากคุณสมบัติเหล่านี้มีเพียงอินพุตเดียวจึงไม่มีอะไรเลยดูเหมือนว่าแผนที่จะมีความซับซ้อนมากกว่าที่จำเป็น)
เมื่อ

โปรดทราบว่ามัณฑนากรทั้งหมดที่มีคนแนะนำว่าเป็นโซลูชัน "นอกกรอบ" ไม่มีอยู่เมื่อฉันถามเช่นกัน
ตั้งแต่

ฉันเห็นด้วยกับ Jason นี่เป็นคำถามเกี่ยวกับการแคช / การบันทึกไม่ใช่การประเมินแบบขี้เกียจ
poindexter

@poindexter - การแคชไม่ค่อยครอบคลุม มันไม่ได้แยกความแตกต่างของการค้นหาค่าในเวลาเริ่มต้นของวัตถุและการแคชจากการค้นหาค่าขึ้นมาและการแคชเมื่อคุณสมบัติถูกเข้าถึง (ซึ่งเป็นคุณสมบัติหลักที่นี่) ฉันควรเรียกมันว่าอะไร? มัณฑนากร "Cache-after-first-use"?
ย้อนกลับ

@ รายละเอียดบันทึก. คุณควรเรียกมันว่า Memoize en.wikipedia.org/wiki/Memoization
poindexter

0

คุณสามารถทำได้ดีและง่ายดายโดยการสร้างคลาสจากคุณสมบัติดั้งเดิมของ Python:

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

เราสามารถใช้คลาสคุณสมบัตินี้เหมือนคุณสมบัติคลาสทั่วไป (นอกจากนี้ยังรองรับการกำหนดไอเท็มตามที่คุณเห็น)

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)

คำนวณมูลค่าในครั้งแรกเท่านั้นและหลังจากนั้นเราใช้ค่าที่บันทึกไว้

เอาท์พุต:

I am calculating value
My calculated value
My calculated value
2
2
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.