มีมัณฑนากรเพียงแค่แคชคืนค่าฟังก์ชันหรือไม่


158

พิจารณาสิ่งต่อไปนี้:

@property
def name(self):

    if not hasattr(self, '_name'):

        # expensive calculation
        self._name = 1 + 1

    return self._name

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

ป.ล. การคำนวณที่แท้จริงไม่ได้ขึ้นอยู่กับค่าที่ไม่แน่นอน


อาจมีมัณฑนากรภายนอกที่มีความสามารถบางอย่างเช่นนั้น แต่คุณไม่ได้ระบุสิ่งที่คุณต้องการอย่างละเอียด คุณใช้แบ็คเอนด์แคชประเภทใด และจะมีการคีย์ค่าอย่างไร ฉันสมมติจากรหัสของคุณว่าสิ่งที่คุณขอจริง ๆ คือคุณสมบัติแคชแบบอ่านอย่างเดียว
David Berger

มีมัณฑนากรช่วยจำที่ทำสิ่งที่คุณเรียกว่า "แคช"; พวกเขามักจะทำงานในฟังก์ชั่นดังกล่าว (ไม่ว่าจะเป็นวิธีการหรือไม่) ซึ่งผลลัพธ์ขึ้นอยู่กับข้อโต้แย้งของพวกเขา (ไม่ได้อยู่ในสิ่งที่ไม่แน่นอนเช่นตัวเอง! -) และเก็บบันทึกแยกต่างหาก
Alex Martelli

คำตอบ:


206

เริ่มต้นจาก Python 3.2 มีมัณฑนากรภายใน:

@functools.lru_cache(maxsize=100, typed=False)

มัณฑนากรเพื่อห่อฟังก์ชั่นที่มีการบันทึกช่วยจำที่สามารถบันทึกได้ถึงการโทรล่าสุดขนาดสูงสุด มันสามารถประหยัดเวลาเมื่อฟังก์ชั่นที่มีราคาแพงหรือ I / O ถูกเรียกเป็นระยะพร้อมกับอาร์กิวเมนต์ที่เหมือนกัน

ตัวอย่างแคช LRU สำหรับการคำนวณหมายเลข Fibonacci :

@lru_cache(maxsize=None)
def fib(n):
    if n < 2:
        return n
    return fib(n-1) + fib(n-2)

>>> print([fib(n) for n in range(16)])
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610]

>>> print(fib.cache_info())
CacheInfo(hits=28, misses=16, maxsize=None, currsize=16)

หากคุณติดกับ Python 2.x ต่อไปนี้เป็นรายชื่อไลบรารีบันทึกช่วยจำอื่น ๆ ที่ใช้งานร่วมกันได้:



backport สามารถพบได้ที่นี่: pypi.python.org/pypi/backports.functools_lru_cache
Frederick Nord

@gerrit ในทางทฤษฎีมันใช้งานได้กับวัตถุที่แฮชโดยทั่วไป - แม้ว่าวัตถุที่แฮชจะมีค่าเท่ากันถ้ามันเป็นวัตถุเดียวกัน (เช่นวัตถุที่ผู้ใช้กำหนดเองโดยไม่มีฟังก์ชัน __hash __ () ที่ชัดเจน)
Jonathan

1
@ โจนาธานมันใช้งานได้ แต่ผิด ถ้าฉันผ่านการโต้แย้ง hashable, ไม่แน่นอนและเปลี่ยนค่าของวัตถุหลังจากการเรียกครั้งแรกของฟังก์ชั่นการเรียกครั้งที่สองจะคืนค่าการเปลี่ยนแปลงไม่ใช่วัตถุดั้งเดิม นั่นคือสิ่งที่ผู้ใช้ต้องการ เพื่อให้สามารถใช้งานอาร์กิวเมนต์ที่ไม่แน่นอนได้จะต้องlru_cacheทำสำเนาผลการแคชใด ๆ และจะไม่มีการทำสำเนาดังกล่าวในfunctools.lru_cacheการนำไปใช้ การทำเช่นนั้นอาจเสี่ยงต่อการสร้างปัญหาหน่วยความจำที่ยากต่อการค้นหาเมื่อใช้เพื่อแคชวัตถุขนาดใหญ่
gerrit

@gerrit คุณจะติดตามได้ที่นี่: stackoverflow.com/questions/44583381/… ? ฉันไม่ได้ทำตามตัวอย่างของคุณทั้งหมด
Jonathan

28

ดูเหมือนว่าคุณไม่ได้ขอมัณฑนากรบันทึกความจำทั่วไป (เช่นคุณไม่สนใจกรณีทั่วไปที่คุณต้องการแคชคืนค่าสำหรับค่าอาร์กิวเมนต์ต่าง ๆ ) นั่นคือคุณต้องการมีสิ่งนี้:

x = obj.name  # expensive
y = obj.name  # cheap

ในขณะที่มัณฑนากรจำช่วยจำทั่วไปมีจุดประสงค์ดังนี้

x = obj.name()  # expensive
y = obj.name()  # cheap

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

[อัพเดท: มัณฑนากรตกแต่งบันทึกช่วยจำตามระดับที่ฉันเชื่อมโยงและอ้างถึงที่นี่ก่อนหน้านี้ใช้ไม่ได้กับวิธีการ ฉันได้แทนที่ด้วยฟังก์ชั่นมัณฑนากร] หากคุณยินดีที่จะใช้มัณฑนากรตกแต่งบันทึกความจำทั่วไปนี่เป็นวิธีง่าย ๆ :

def memoize(function):
  memo = {}
  def wrapper(*args):
    if args in memo:
      return memo[args]
    else:
      rv = function(*args)
      memo[args] = rv
      return rv
  return wrapper

ตัวอย่างการใช้งาน:

@memoize
def fibonacci(n):
  if n < 2: return n
  return fibonacci(n - 1) + fibonacci(n - 2)

มัณฑนากร memoization อื่นที่มีข้อ จำกัด เรื่องขนาดของแคชสามารถพบได้ที่นี่


ไม่มีการตกแต่งภายในที่กล่าวถึงในคำตอบทั้งหมดที่ใช้สำหรับวิธีการ! อาจเป็นเพราะพวกเขามีระดับพื้นฐาน มีเพียงตัวเดียวเท่านั้นที่ผ่านไปได้? คนอื่นทำงานได้ดี แต่มันก็โหดร้ายที่เก็บค่าไว้ในฟังก์ชั่น
Tobias

2
ฉันคิดว่าคุณอาจพบปัญหาหาก args ไม่แฮช
ไม่ทราบ

1
@ ไม่ทราบใช่มัณฑนากรคนแรกที่ฉันอ้างถึงที่นี่ถูก จำกัด ประเภทแฮช หนึ่งรายการที่ ActiveState (ที่มีขีด จำกัด ขนาดแคช) จะเลือกอาร์กิวเมนต์เป็นสตริง (hashable) ซึ่งแน่นอนว่ามีราคาแพงกว่า แต่มีความกว้างกว่า
นาธานครัว

@vanity ขอบคุณสำหรับการชี้ให้เห็นข้อ จำกัด ของการตกแต่งตามระดับ ฉันได้แก้ไขคำตอบของฉันเพื่อแสดงฟังก์ชั่นมัณฑนากรซึ่งใช้ได้กับวิธีการ (ฉันทดสอบอันนี้จริงๆ)
นาธานครัว

1
@SiminJie มัณฑนากรเรียกเพียงครั้งเดียวและฟังก์ชั่นที่ห่อแล้วจะส่งคืนเป็นฟังก์ชันเดียวที่ใช้สำหรับการโทรที่แตกต่างกันfibonacciทั้งหมด ฟังก์ชันนั้นใช้memoพจนานุกรมเดียวกันเสมอ
นาธานคิทเช่น

22
class memorize(dict):
    def __init__(self, func):
        self.func = func

    def __call__(self, *args):
        return self[args]

    def __missing__(self, key):
        result = self[key] = self.func(*key)
        return result

ตัวอย่างการใช้งาน:

>>> @memorize
... def foo(a, b):
...     return a * b
>>> foo(2, 4)
8
>>> foo
{(2, 4): 8}
>>> foo('hi', 3)
'hihihi'
>>> foo
{(2, 4): 8, ('hi', 3): 'hihihi'}

แปลก! มันทำงานอย่างไร มันไม่ได้ดูเหมือนนักตกแต่งอื่น ๆ ที่ฉันเคยเห็น
PascalVKooten

1
วิธีนี้จะคืนค่า TypeError หากมีการใช้อาร์กิวเมนต์คำหลักเช่น foo (3, b = 5)
kadee

1
ปัญหาของการแก้ปัญหาคือมันไม่มีขีด จำกัด หน่วยความจำ สำหรับอาร์กิวเมนต์ที่มีชื่อคุณสามารถเพิ่มพวกเขาใน __ call__ และ __ missing__ เช่น ** nargs
Leonid Mednikov

16

เครื่องมือfunctools.cached_propertyตกแต่งPython 3.8

https://docs.python.org/dev/library/functools.html#functools.cached_property

cached_propertyจาก Werkzeug ถูกกล่าวถึงที่: https://stackoverflow.com/a/5295190/895245แต่เวอร์ชันที่ได้มาที่คาดคะเนจะถูกรวมเข้าเป็น 3.8 ซึ่งยอดเยี่ยมมาก

มัณฑนากรนี้สามารถเห็นได้ว่าแคช@propertyหรือเป็นตัวทำความสะอาด @functools.lru_cacheเมื่อคุณไม่มีข้อโต้แย้งใด ๆ

เอกสารบอกว่า:

@functools.cached_property(func)

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

ตัวอย่าง:

class DataSet:
    def __init__(self, sequence_of_numbers):
        self._data = sequence_of_numbers

    @cached_property
    def stdev(self):
        return statistics.stdev(self._data)

    @cached_property
    def variance(self):
        return statistics.variance(self._data)

ใหม่ในเวอร์ชัน 3.8

หมายเหตุมัณฑนากรนี้ต้องการให้แอตทริบิวต์dictในแต่ละอินสแตนซ์เป็นการจับคู่ที่ไม่แน่นอน ซึ่งหมายความว่ามันจะไม่ทำงานกับบางประเภทเช่น metaclasses (เนื่องจากแอตทริบิวต์dictบนอินสแตนซ์ชนิดเป็นพรอกซีอ่านอย่างเดียวสำหรับ namespace ของคลาส) และที่ระบุสล็อตโดยไม่รวมdictเป็นหนึ่งในสล็อตที่กำหนด ไม่ต้องระบุแอตทริบิวต์dictเลย)


10

Werkzeug มีcached_propertyมัณฑนากร ( เอกสาร , แหล่งที่มา )


ใช่. นี่เป็นสิ่งที่คุ้มค่าที่จะแยกความแตกต่างจากกรณีการจำทั่วไปเนื่องจากการบันทึกมาตรฐานจะไม่ทำงานหากคลาสนั้นไม่แฮช
Jameson Quinn


9

ฉันเขียนคลาสมัณฑนากรง่าย ๆ นี้เพื่อแคชการตอบสนองของฟังก์ชั่น ฉันพบว่ามีประโยชน์มากสำหรับโครงการของฉัน:

from datetime import datetime, timedelta 

class cached(object):
    def __init__(self, *args, **kwargs):
        self.cached_function_responses = {}
        self.default_max_age = kwargs.get("default_cache_max_age", timedelta(seconds=0))

    def __call__(self, func):
        def inner(*args, **kwargs):
            max_age = kwargs.get('max_age', self.default_max_age)
            if not max_age or func not in self.cached_function_responses or (datetime.now() - self.cached_function_responses[func]['fetch_time'] > max_age):
                if 'max_age' in kwargs: del kwargs['max_age']
                res = func(*args, **kwargs)
                self.cached_function_responses[func] = {'data': res, 'fetch_time': datetime.now()}
            return self.cached_function_responses[func]['data']
        return inner

การใช้งานตรงไปตรงมา:

import time

@cached
def myfunc(a):
    print "in func"
    return (a, datetime.now())

@cached(default_max_age = timedelta(seconds=6))
def cacheable_test(a):
    print "in cacheable test: "
    return (a, datetime.now())


print cacheable_test(1,max_age=timedelta(seconds=5))
print cacheable_test(2,max_age=timedelta(seconds=5))
time.sleep(7)
print cacheable_test(3,max_age=timedelta(seconds=5))

1
ครั้งแรกของคุณ@cachedไม่มีวงเล็บ มิฉะนั้นจะส่งคืนcachedวัตถุแทนmyfuncและเมื่อถูกเรียกเมื่อmyfunc()นั้นinnerจะถูกส่งคืนเป็นค่าที่ส่งคืนเสมอ
Markus Meskanen

6

หมายเหตุ:ผมผู้เขียนของkids.cache

คุณควรตรวจสอบkids.cacheมันมี@cacheมัณฑนากรที่ทำงานบน python 2 และ python 3 ไม่มีการพึ่งพารหัส ~ 100 บรรทัด ตัวอย่างเช่นการใช้งานโค้ดที่ตรงไปตรงมาโดยที่คุณนึกได้ว่าคุณสามารถใช้สิ่งนี้:

pip install kids.cache

แล้วก็

from kids.cache import cache
...
class MyClass(object):
    ...
    @cache            # <-- That's all you need to do
    @property
    def name(self):
        return 1 + 1  # supposedly expensive calculation

หรือคุณสามารถวาง@cacheมัณฑนากรตามหลัง@property(ผลลัพธ์เดียวกัน)

การใช้แคชในสถานที่ที่เรียกว่าการประเมินผลขี้เกียจ , kids.cacheสามารถทำมากขึ้น (มันทำงานในฟังก์ชั่นที่มีการขัดแย้งใด ๆ คุณสมบัติประเภทของวิธีการใด ๆ และแม้กระทั่งเรียน ... ) สำหรับผู้ใช้ขั้นสูงkids.cacheรองรับการcachetoolsจัดเก็บแคชแฟนซีไปยัง python 2 และ python 3 (แคช LRU, LFU, TTL, RR)

หมายเหตุสำคัญ : ที่เก็บแคชเริ่มต้นของkids.cacheเป็น dict มาตรฐานซึ่งไม่แนะนำให้ใช้กับโปรแกรมที่ใช้งานมานานซึ่งมีคิวรีที่แตกต่างกันเนื่องจากจะนำไปสู่ที่เก็บแคชที่เพิ่มขึ้นเรื่อย ๆ สำหรับการใช้งานนี้คุณสามารถปลั๊กอินร้านค้าแคชอื่น ๆ ที่ใช้เช่น ( @cache(use=cachetools.LRUCache(maxsize=2))เพื่อตกแต่งฟังก์ชั่น / ทรัพย์สิน / คลาส / วิธีการของคุณ ... )


โมดูลนี้ส่งผลให้เวลาในการนำเข้าช้าลงใน python 2 ~ 0.9s (ดู: pastebin.com/raw/aA1ZBE9Z ) ฉันสงสัยว่านี่เป็นเพราะสายนี้github.com/0k/kids.cache/blob/master/src/kids/__init__.py#L3 (จุดเข้า cf setuptools) ฉันกำลังสร้างปัญหาสำหรับสิ่งนี้
Att Righ

นี่คือปัญหาสำหรับข้างต้นgithub.com/0k/kids.cache/issues/9
Att Righ

สิ่งนี้จะนำไปสู่การรั่วไหลของหน่วยความจำ
Timothy Zhang

@vaab สร้างอินสแตนซ์cของMyClassและตรวจสอบได้ด้วยobjgraph.show_backrefs([c], max_depth=10)มีความเป็นห่วงโซ่เตะจากวัตถุชั้นไปMyClass cกล่าวคือcจะไม่ได้รับการปล่อยตัวจนกว่าจะMyClassถูกปล่อยตัวออกมา
Timothy Zhang

@TimothyZhang คุณได้รับเชิญและยินดีที่จะเพิ่มความกังวลของคุณในgithub.com/0k/kids.cache/issues/10 Stackoverflow ไม่ใช่สถานที่ที่เหมาะสมสำหรับการอภิปรายที่เหมาะสม และต้องการความกระจ่างเพิ่มเติม ขอบคุณสำหรับความคิดเห็นของคุณ.
vaab


4

มีfastcacheซึ่งเป็น "การใช้งาน C ของ Python 3 functools.lru_cache ให้ความเร็ว 10-30x บนไลบรารี่มาตรฐาน"

เหมือนกับคำตอบที่เลือกการนำเข้าที่แตกต่างกันเพียง:

from fastcache import lru_cache
@lru_cache(maxsize=128, typed=False)
def f(a, b):
    pass

นอกจากนี้ก็มาติดตั้งในงูซึ่งแตกต่างจาก functools ซึ่งจะต้องมีการติดตั้ง


1
functoolsเป็นส่วนหนึ่งของไลบรารีมาตรฐานลิงก์ที่คุณโพสต์คือการแยกคอมไพล์แบบสุ่มหรืออย่างอื่น ...
cz

3

ยังมีตัวอย่างของอีกmemoizeมัณฑนากรที่หลามวิกิพีเดีย:

http://wiki.python.org/moin/PythonDecoratorLibrary#Memoize

ตัวอย่างนั้นค่อนข้างฉลาดเพราะมันจะไม่แคชผลลัพธ์หากพารามิเตอร์ไม่แน่นอน (ตรวจสอบรหัสนั้นมันง่ายมากและน่าสนใจ!)


3

หากคุณใช้ Django Framework ก็มีคุณสมบัติดังกล่าวที่จะแคชมุมมองหรือการตอบสนองของการใช้ API @cache_page(time)และอาจมีตัวเลือกอื่นเช่นกัน

ตัวอย่าง:

@cache_page(60 * 15, cache="special_cache")
def my_view(request):
    ...

รายละเอียดเพิ่มเติมสามารถพบได้ที่นี่


2

พร้อมกับตัวอย่างบันทึกฉันพบแพ็คเกจหลามต่อไปนี้:

  • cachepy ; จะช่วยให้การตั้งค่า ttl และ \ หรือจำนวนการโทรสำหรับฟังก์ชั่นแคช; นอกจากนี้หนึ่งสามารถใช้แคชไฟล์ที่เข้ารหัส ...
  • percache

1

ฉันใช้บางสิ่งเช่นนี้โดยใช้ดองเพื่อการคงอยู่และใช้ sha1 สำหรับรหัสประจำตัวสั้น ๆ โดยทั่วไปแคชจะแฮชโค้ดของฟังก์ชันและฮิสต์ของอาร์กิวเมนต์เพื่อรับ sha1 จากนั้นค้นหาไฟล์ที่มี sha1 นั้นในชื่อ หากมีอยู่มันจะเปิดและส่งคืนผลลัพธ์ ถ้าไม่มันจะเรียกฟังก์ชั่นและบันทึกผลลัพธ์ (เลือกที่จะบันทึกเฉพาะในกรณีที่ใช้เวลาในการประมวลผล)

ที่กล่าวว่าฉันสาบานฉันพบโมดูลที่มีอยู่ที่ทำสิ่งนี้และพบว่าตัวเองกำลังพยายามค้นหาโมดูลนั้น ... สิ่งที่ฉันสามารถค้นหาได้ใกล้ที่สุดคือสิ่งนี้ซึ่งมีลักษณะที่ถูกต้อง: http: //chase-seibert.github io / บล็อก / 2011/11/23 / pythondjango ดิสก์ตามแคช-decorator.html

ปัญหาเดียวที่ฉันเห็นคือมันใช้งานไม่ได้กับอินพุตขนาดใหญ่เพราะมันแฮ็ก STR (ARG) ซึ่งไม่เหมือนกันสำหรับอาร์เรย์ขนาดใหญ่

มันจะดีถ้ามีโปรโตคอลunique_hash () ที่ให้คลาสส่งคืนแฮชที่ปลอดภัยของเนื้อหา โดยทั่วไปฉันใช้งานด้วยตนเองสำหรับประเภทที่ฉันใส่ใจ



1

หากคุณกำลังใช้ Django และต้องการที่จะมองเห็นวิวแคชดูคำตอบของ Nikhil Kumar


แต่ถ้าคุณต้องการที่จะเก็บค่าผลลัพธ์ฟังก์ชั่นใด ๆ คุณสามารถใช้Django-cache-utils

มันนำแคช Django มาใช้ใหม่และนำเสนอcachedมัณฑนากรที่ใช้งานง่าย:

from cache_utils.decorators import cached

@cached(60)
def foo(x, y=0):
    print 'foo is called'
    return x+y

1

@lru_cache ไม่สมบูรณ์แบบด้วยค่าฟังก์ชั่นเริ่มต้น

memมัณฑนากรของฉัน:

import inspect


def get_default_args(f):
    signature = inspect.signature(f)
    return {
        k: v.default
        for k, v in signature.parameters.items()
        if v.default is not inspect.Parameter.empty
    }


def full_kwargs(f, kwargs):
    res = dict(get_default_args(f))
    res.update(kwargs)
    return res


def mem(func):
    cache = dict()

    def wrapper(*args, **kwargs):
        kwargs = full_kwargs(func, kwargs)
        key = list(args)
        key.extend(kwargs.values())
        key = hash(tuple(key))
        if key in cache:
            return cache[key]
        else:
            res = func(*args, **kwargs)
            cache[key] = res
            return res
    return wrapper

และรหัสสำหรับการทดสอบ:

from time import sleep


@mem
def count(a, *x, z=10):
    sleep(2)
    x = list(x)
    x.append(z)
    x.append(a)
    return sum(x)


def main():
    print(count(1,2,3,4,5))
    print(count(1,2,3,4,5))
    print(count(1,2,3,4,5, z=6))
    print(count(1,2,3,4,5, z=6))
    print(count(1))
    print(count(1, z=10))


if __name__ == '__main__':
    main()

ผล - เพียง 3 ครั้งด้วยการนอนหลับ

แต่ด้วย@lru_cacheจะเป็น 4 ครั้งเพราะ:

print(count(1))
print(count(1, z=10))

จะถูกคำนวณสองครั้ง (ใช้งานไม่ได้กับค่าเริ่มต้น)

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