การบันทึกความจำคืออะไรและฉันจะใช้ใน Python ได้อย่างไร


378

ฉันเพิ่งเริ่ม Python และฉันก็ไม่รู้ว่าการบันทึกคืออะไรและใช้อย่างไร นอกจากนี้ฉันขอยกตัวอย่างแบบง่าย ๆ ได้ไหม


215
เมื่อประโยคที่สองของบทความ wikipedia ที่เกี่ยวข้องมีวลี "การแยกวิเคราะห์แบบเรียกซ้ำ - ซ้ำ [1] ในอัลกอริทึมการแยกวิเคราะห์แบบบนลงล่างทั่วไป [2] [3] ที่รองรับความกำกวมและการเรียกซ้ำในเวลาและพื้นที่พหุนาม" มันเหมาะสมอย่างยิ่งที่จะถามว่าเกิดอะไรขึ้น
Clueless

10
@Clueless: วลีนั้นนำหน้าด้วย "การบันทึกในบริบทอื่น ๆ (และเพื่อวัตถุประสงค์อื่นนอกเหนือจากการเพิ่มความเร็ว) เช่นใน" ดังนั้นจึงเป็นเพียงรายการตัวอย่าง (และไม่จำเป็นต้องเข้าใจ); มันไม่ได้เป็นส่วนหนึ่งของคำอธิบายของการบันทึก
ShreevatsaR

1
@StefanGruenwald ลิงก์นั้นตาย คุณช่วยหาการอัพเดทได้ไหม?
JS

2
ลิงก์ใหม่ไปยังไฟล์ PDF เนื่องจาก pycogsci.info ไม่ทำงาน: people.ucsc.edu/~abrsvn/NLTK_parsing_demos.pdf
Stefan Gruenwald

4
@ Clueless, บทความจริง ๆ แล้วพูดว่า " ง่าย ๆร่วมกัน - สืบเชื้อสายมาแยกวิเคราะห์ซ้ำ [1] ในอัลกอริธึมการแยกวิเคราะห์จากบนลงล่าง [2] [3] ที่รองรับความกำกวมและการเรียกซ้ำในเวลาพหุนามและอวกาศ" คุณพลาดความเรียบง่ายซึ่งเห็นได้ชัดว่าเป็นตัวอย่างที่ชัดเจนมากขึ้น :)
studgeek

คำตอบ:


353

การบันทึกอย่างมีประสิทธิภาพหมายถึงการจดจำ ("การบันทึกความจำ" → "บันทึกความจำ" →ที่ต้องจดจำ) ผลลัพธ์ของการเรียกใช้เมธอดตามวิธีการอินพุตจากนั้นส่งคืนผลลัพธ์ที่จำได้มากกว่าการคำนวณผลลัพธ์อีกครั้ง คุณสามารถคิดว่ามันเป็นแคชสำหรับผลลัพธ์ของวิธีการ สำหรับรายละเอียดเพิ่มเติมดูหน้า 387 สำหรับคำจำกัดความในบทนำสู่อัลกอริทึม (3e), Cormen และคณะ

ตัวอย่างง่ายๆสำหรับการคำนวณแฟคทอเรียลโดยใช้การบันทึกใน Python จะเป็นดังนี้:

factorial_memo = {}
def factorial(k):
    if k < 2: return 1
    if k not in factorial_memo:
        factorial_memo[k] = k * factorial(k-1)
    return factorial_memo[k]

คุณสามารถเพิ่มความซับซ้อนและห่อหุ้มกระบวนการบันทึกความจำลงในคลาสได้:

class Memoize:
    def __init__(self, f):
        self.f = f
        self.memo = {}
    def __call__(self, *args):
        if not args in self.memo:
            self.memo[args] = self.f(*args)
        #Warning: You may wish to do a deepcopy here if returning objects
        return self.memo[args]

แล้ว:

def factorial(k):
    if k < 2: return 1
    return k * factorial(k - 1)

factorial = Memoize(factorial)

มีการเพิ่มฟีเจอร์ที่เรียกว่า " decorators " ใน Python 2.4 ซึ่งช่วยให้คุณสามารถเขียนสิ่งต่อไปนี้เพื่อทำสิ่งเดียวกันให้สำเร็จ:

@Memoize
def factorial(k):
    if k < 2: return 1
    return k * factorial(k - 1)

มัณฑนากรห้องสมุดหลามมีมัณฑนากรที่คล้ายกันเรียกmemoizedว่าเป็นเพียงเล็กน้อยที่แข็งแกร่งมากขึ้นกว่าMemoizeระดับที่แสดงที่นี่


2
ขอบคุณสำหรับคำแนะนำนี้ คลาส Memoize เป็นโซลูชันสุดหรูที่สามารถนำไปใช้กับรหัสที่มีอยู่ได้อย่างง่ายดายโดยไม่จำเป็นต้องทำการปรับเปลี่ยนใหม่มากนัก
Captain Lepton

10
วิธีการแก้ปัญหาระดับ memoize เป็นรถก็จะไม่ทำงานเช่นเดียวกับfactorial_memoเพราะfactorialภายในdef factorialยังคงเรียก factorialunmemoize
adamsmith

9
โดยวิธีการที่คุณยังสามารถเขียนซึ่งอ่านดีกว่าif k not in factorial_memo: if not k in factorial_memo:
ShreevatsaR

5
ควรทำเช่นนี้เป็นมัณฑนากร
Emlyn O'Regan

3
@ durden2.0 ฉันรู้ว่านี่เป็นความคิดเห็นเก่า แต่argsเป็นสิ่งอันดับ def some_function(*args)ทำให้สิ่ง tuple
Adam Smith

232

ใหม่เพื่อหลาม 3.2 functools.lru_cacheคือ โดยค่าเริ่มต้นเพียงแคช 128 สายส่วนใหญ่ใช้ในเร็ว ๆ นี้ แต่คุณสามารถตั้งค่าmaxsizeที่จะNoneชี้ให้เห็นว่าแคชไม่ควรหมดอายุ:

import functools

@functools.lru_cache(maxsize=None)
def fib(num):
    if num < 2:
        return num
    else:
        return fib(num-1) + fib(num-2)

ฟังก์ชั่นนี้ช้ามาก ๆ ลองfib(36)แล้วคุณจะต้องรอประมาณสิบวินาที

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


2
Fib ที่พยายาม (1,000), ได้รับ RecursionError: ความลึกของการเรียกซ้ำสูงสุดเกินกว่าที่เปรียบเทียบ
X Æ A-12

5
@Andyk ขีด จำกัด การเรียกซ้ำ Py3 เริ่มต้นที่ 1,000 คือครั้งแรกที่fibมีการเรียกมันจะต้องเกิดขึ้นอีกครั้งในกรณีพื้นฐานก่อนที่การบันทึกจะเกิดขึ้นได้ ดังนั้นพฤติกรรมของคุณเป็นไปตามคาด
Quelklef

1
ถ้าฉันไม่เข้าใจผิดมันจะเก็บเฉพาะจนกว่ากระบวนการจะไม่ฆ่าใช่มั้ย หรือมันแคชโดยไม่คำนึงว่ากระบวนการถูกฆ่าตาย? เช่นบอกว่าฉันรีสตาร์ทระบบ - ผลลัพธ์แคชจะยังคงถูกแคชอยู่หรือไม่?
Kristada673

1
@ Kristada673 ใช่มันถูกเก็บไว้ในหน่วยความจำของกระบวนการไม่ใช่ในดิสก์
Flimm

2
โปรดทราบว่าสิ่งนี้จะเพิ่มความเร็วแม้การเรียกใช้ครั้งแรกของฟังก์ชันเนื่องจากเป็นฟังก์ชันแบบเรียกซ้ำและแคชผลลัพธ์กลางของตัวเอง อาจเป็นการดีที่จะแสดงให้เห็นถึงฟังก์ชั่นที่ไม่เกิดซ้ำซึ่งเป็นเพียงการกำเนิดช้าเพื่อให้ชัดเจนกับหุ่นเช่นฉัน : D
endolith

61

คำตอบอื่น ๆ ครอบคลุมถึงสิ่งที่ค่อนข้างดี ฉันไม่ได้ทำซ้ำ เพียงบางจุดที่อาจเป็นประโยชน์กับคุณ

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

memoised_function = memoise(actual_function)

หรือแสดงเป็นมัณฑนากร

@memoise
def actual_function(arg1, arg2):
   #body

18

การบันทึกคือการเก็บผลลัพธ์ของการคำนวณที่มีราคาแพงและส่งคืนผลลัพธ์ที่แคชไว้แทนที่จะคำนวณใหม่อย่างต่อเนื่อง

นี่คือตัวอย่าง:

def doSomeExpensiveCalculation(self, input):
    if input not in self.cache:
        <do expensive calculation>
        self.cache[input] = result
    return self.cache[input]

คำอธิบายที่สมบูรณ์มากขึ้นสามารถพบได้ในรายการวิกิพีเดีย memoization


อืมตอนนี้ถ้านั่นเป็น Python ที่ถูกต้องมันน่าจะสั่น แต่ดูเหมือนว่าจะไม่ ... โอเคดังนั้น "แคช" ไม่ใช่คำสั่ง? เพราะถ้ามันเป็นเช่นนั้นมันควรจะเป็น if input not in self.cache และ self.cache[input] ( has_keyล้าสมัยตั้งแต่ ... ในช่วงต้นของซีรีส์ 2.x ถ้าไม่ใช่ 2.0 self.cache(index)ไม่เคยถูกแก้ไข IIRC)
Jürgen A. Erhard

15

อย่าลืมhasattrฟังก์ชั่นในตัวสำหรับผู้ที่ต้องการส่งงานฝีมือ ด้วยวิธีนี้คุณสามารถเก็บแคช mem ไว้ในนิยามของฟังก์ชัน (ตรงข้ามกับส่วนกลาง)

def fact(n):
    if not hasattr(fact, 'mem'):
        fact.mem = {1: 1}
    if not n in fact.mem:
        fact.mem[n] = n * fact(n - 1)
    return fact.mem[n]

ดูเหมือนว่าความคิดราคาแพงมาก สำหรับทุก ๆ n ไม่เพียง แต่แคชผลลัพธ์สำหรับ n แต่สำหรับ 2 ... n-1
codeforester

15

ฉันพบว่ามีประโยชน์มาก

def memoize(function):
    from functools import wraps

    memo = {}

    @wraps(function)
    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)

fibonacci(25)

ดูdocs.python.org/3/library/functools.html#functools.wrapsfunctools.wrapsสำหรับเหตุผลที่ควรใช้
anishpatel

1
ฉันต้องล้างข้อมูลด้วยตนเองmemoเพื่อให้หน่วยความจำว่างหรือไม่?
nos

แนวคิดทั้งหมดคือผลลัพธ์จะถูกเก็บไว้ภายในบันทึกภายในเซสชัน คือไม่มีอะไรถูกลบล้างอย่างที่มันเป็น
mr.bjerre

6

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

ดูhttp://scriptbucket.wordpress.com/2012/12/11/introduction-to-memoization/

ตัวอย่างการจำ Fibonacci ใน Python:

fibcache = {}
def fib(num):
    if num in fibcache:
        return fibcache[num]
    else:
        fibcache[num] = num if num < 2 else fib(num-1) + fib(num-2)
        return fibcache[num]

2
เพื่อประสิทธิภาพที่สูงกว่าการจัดเก็บข้อมูล fibcache ของคุณล่วงหน้าด้วยค่าที่รู้จักสองสามอันดับแรกจากนั้นคุณสามารถใช้ตรรกะพิเศษในการจัดการพวกเขาออกจาก 'เส้นทางที่ร้อนแรง' ของรหัส
jkflying

5

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


5

ฉันควรตอบส่วนแรกก่อน: การบันทึกความจำคืออะไร?

มันเป็นเพียงวิธีการในการแลกเปลี่ยนความทรงจำในเวลา คิดว่าตารางการคูณ

การใช้วัตถุที่ไม่แน่นอนเป็นค่าเริ่มต้นใน Python มักจะถือว่าไม่ดี memoizationแต่ถ้าใช้มันอย่างชาญฉลาดก็จริงจะมีประโยชน์ในการดำเนินการ

นี่คือตัวอย่างที่ดัดแปลงมาจากhttp://docs.python.org/2/faq/design.html#why-are-default-values-shared-between-objects

การใช้ความไม่แน่นอนdictในการกำหนดฟังก์ชั่นผลลัพธ์ที่คำนวณได้กลางสามารถแคช (เช่นเมื่อคำนวณfactorial(10)หลังจากการคำนวณfactorial(9)เราสามารถนำผลกลางทั้งหมดกลับมาใช้ใหม่ได้)

def factorial(n, _cache={1:1}):    
    try:            
        return _cache[n]           
    except IndexError:
        _cache[n] = factorial(n-1)*n
        return _cache[n]

4

นี่เป็นวิธีการแก้ปัญหาที่จะทำงานกับรายการหรือข้อโต้แย้งประเภท dict โดยไม่ต้องเสียงหอน:

def memoize(fn):
    """returns a memoized version of any function that can be called
    with the same list of arguments.
    Usage: foo = memoize(foo)"""

    def handle_item(x):
        if isinstance(x, dict):
            return make_tuple(sorted(x.items()))
        elif hasattr(x, '__iter__'):
            return make_tuple(x)
        else:
            return x

    def make_tuple(L):
        return tuple(handle_item(x) for x in L)

    def foo(*args, **kwargs):
        items_cache = make_tuple(sorted(kwargs.items()))
        args_cache = make_tuple(args)
        if (args_cache, items_cache) not in foo.past_calls:
            foo.past_calls[(args_cache, items_cache)] = fn(*args,**kwargs)
        return foo.past_calls[(args_cache, items_cache)]
    foo.past_calls = {}
    foo.__name__ = 'memoized_' + fn.__name__
    return foo

โปรดทราบว่าวิธีนี้สามารถขยายไปยังวัตถุใด ๆ โดยธรรมชาติโดยการใช้ฟังก์ชั่นแฮชของคุณเองเป็นกรณีพิเศษใน handle_item ตัวอย่างเช่นในการทำให้วิธีการนี้ใช้งานได้กับฟังก์ชั่นที่รับชุดเป็นอาร์กิวเมนต์อินพุตคุณสามารถเพิ่มไปยัง handle_item:

if is_instance(x, set):
    return make_tuple(sorted(list(x)))

1
ความพยายามที่ดี โดยไม่ต้องเสียงหอนเป็นlistข้อโต้แย้งของ[1, 2, 3]สามารถผิดพลาดได้รับการพิจารณาเช่นเดียวกับที่แตกต่างกันทะเลาะกับค่าของset นอกจากนี้ชุดมีการเรียงลำดับเช่นพจนานุกรมเพื่อให้พวกเขายังจะต้อง{1, 2, 3} sorted()โปรดสังเกตว่าอาร์กิวเมนต์โครงสร้างข้อมูลแบบเรียกซ้ำจะทำให้เกิดการวนซ้ำไม่สิ้นสุด
martineau

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

ปัญหาที่ฉันกล่าวถึงเนื่องจากความจริงที่ว่าlists และsets นั้น "tupleized" เป็นสิ่งเดียวกันและดังนั้นจึงแยกไม่ออกจากกัน ตัวอย่างโค้ดสำหรับเพิ่มการสนับสนุนตามที่setsอธิบายไว้ในการอัปเดตล่าสุดของคุณไม่ได้หลีกเลี่ยงที่ฉันกลัว สิ่งนี้สามารถมองเห็นได้อย่างง่ายดายโดยผ่านการแยก[1,2,3]และ{1,2,3}เป็นอาร์กิวเมนต์ของฟังก์ชันทดสอบ "memoize" และดูว่ามันถูกเรียกสองครั้งตามที่ควรหรือไม่
martineau

ใช่ฉันอ่านปัญหานั้น แต่ฉันไม่ได้พูดถึงเพราะฉันคิดว่ามันเล็กกว่าที่คุณพูดถึงอีกมาก เมื่อครั้งสุดท้ายที่คุณเขียนฟังก์ชั่นบันทึกความทรงจำที่อาร์กิวเมนต์คงที่อาจเป็นรายการหรือชุดและทั้งสองทำให้เกิดผลลัพธ์ที่แตกต่างกัน? หากคุณต้องพบเจอกับกรณีที่เกิดขึ้นได้ยากคุณจะต้องเขียน handle_item อีกครั้งเพื่อเติมก่อนพูด 0 ถ้าองค์ประกอบนั้นเป็นชุดหรือ 1 ถ้าเป็นรายการ
RussellStewart

ที่จริงแล้วมีปัญหาที่คล้ายกันกับlists และdicts เพราะเป็นไปได้ที่listจะมีสิ่งเดียวกันในนั้นซึ่งเป็นผลมาจากการเรียกmake_tuple(sorted(x.items()))พจนานุกรม วิธีแก้ปัญหาอย่างง่ายสำหรับทั้งสองกรณีคือการรวมtype()ค่าไว้ใน tuple ที่สร้างขึ้น ฉันสามารถคิดถึงวิธีที่ง่ายกว่าในการจัดการsets แต่มันไม่ได้พูดถึง
martineau

3

โซลูชันที่ทำงานกับทั้งอาร์กิวเมนต์ตำแหน่งและคำหลักโดยไม่ขึ้นกับลำดับที่ args คำหลักถูกส่งผ่าน (ใช้inspect.getargspec ):

import inspect
import functools

def memoize(fn):
    cache = fn.cache = {}
    @functools.wraps(fn)
    def memoizer(*args, **kwargs):
        kwargs.update(dict(zip(inspect.getargspec(fn).args, args)))
        key = tuple(kwargs.get(k, None) for k in inspect.getargspec(fn).args)
        if key not in cache:
            cache[key] = fn(**kwargs)
        return cache[key]
    return memoizer

คำถามที่คล้ายกัน: การระบุฟังก์ชัน varargs ที่เทียบเท่ากันจะเรียกใช้การบันทึกความจำใน Python


2
cache = {}
def fib(n):
    if n <= 1:
        return n
    else:
        if n not in cache:
            cache[n] = fib(n-1) + fib(n-2)
        return cache[n]

4
คุณสามารถใช้เพียงแค่if n not in cacheแทน การใช้cache.keysจะสร้างรายการที่ไม่จำเป็นใน python 2
n611x007

2

แค่อยากจะเพิ่มคำตอบที่มีให้อยู่แล้วห้องสมุดมัณฑนากรหลามมีบางอย่างง่าย ๆ การใช้งานที่มีประโยชน์ที่ยังสามารถ memoize "ประเภท unhashable" functools.lru_cacheซึ่งแตกต่างจาก


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