Ghi nhớ là gì và làm thế nào tôi có thể sử dụng nó trong Python?


378

Tôi mới bắt đầu Python và tôi không biết ghi nhớ là gì và cách sử dụng nó. Ngoài ra, tôi có thể có một ví dụ đơn giản?


215
Khi câu thứ hai của bài viết trên wikipedia có liên quan chứa cụm từ "phân tích cú pháp gốc đệ quy [1] trong thuật toán phân tích cú pháp từ trên xuống chung [2] [3] phù hợp với sự mơ hồ và đệ quy trái trong thời gian và không gian đa thức", tôi nghĩ nó hoàn toàn thích hợp để hỏi SO những gì đang xảy ra.
Cluless

10
@Cluless: Cụm từ đó có trước "Ghi nhớ cũng đã được sử dụng trong các bối cảnh khác (và cho các mục đích khác ngoài tốc độ tăng), chẳng hạn như trong". Vì vậy, nó chỉ là một danh sách các ví dụ (và không cần phải hiểu); nó không phải là một phần của lời giải thích
ShreevatsaR

1
@StefanGruenwald Liên kết đó đã chết. Bạn có thể vui lòng tìm một bản cập nhật?
JS.

2
Liên kết mới đến tệp pdf, vì pycogsci.info không hoạt động: people.ucsc.edu/~abrsvn/NLTK_parsing_demos.pdf
Stefan Gruenwald

4
@Cluless, Bài báo thực sự nói " phân tích cú pháp gốc đệ quy đơn giản [1] trong thuật toán phân tích cú pháp từ trên xuống chung [2] [3] phù hợp với sự mơ hồ và đệ quy trái trong thời gian và không gian đa thức". Bạn đã bỏ lỡ đơn giản , điều này rõ ràng làm cho ví dụ đó rõ ràng hơn nhiều :).
studgeek

Câu trả lời:


353

Ghi nhớ có hiệu quả liên quan đến việc ghi nhớ ("ghi nhớ" → "ghi nhớ" → được ghi nhớ) kết quả của các cuộc gọi phương thức dựa trên các đầu vào phương thức và sau đó trả về kết quả đã nhớ thay vì tính lại kết quả. Bạn có thể nghĩ về nó như một bộ đệm cho kết quả phương pháp. Để biết thêm chi tiết, xem trang 387 để biết định nghĩa trong Giới thiệu về thuật toán (3e), Cormen et al.

Một ví dụ đơn giản để tính toán các yếu tố sử dụng ghi nhớ trong Python sẽ giống như thế này:

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]

Bạn có thể trở nên phức tạp hơn và gói gọn quá trình ghi nhớ vào một lớp:

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]

Sau đó:

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

factorial = Memoize(factorial)

Một tính năng được gọi là " trang trí " đã được thêm vào Python 2.4, cho phép bạn chỉ cần viết những điều sau đây để thực hiện điều tương tự:

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

Các Python Decorator Thư viện có trang trí tương tự gọi memoizedđó là hơi mạnh hơn so với các Memoizelớp trình bày ở đây.


2
Cảm ơn đề nghị này. Lớp Memize là một giải pháp tao nhã có thể dễ dàng áp dụng cho mã hiện có mà không cần tái cấu trúc nhiều.
Thuyền trưởng Lepton

10
Giải pháp lớp Ghi nhớ là lỗi, nó sẽ không hoạt động giống như factorial_memo, bởi vì factorialbên trong def factorialvẫn gọi là không cũ factorial.
adamsmith

9
Nhân tiện, bạn cũng có thể viết if k not in factorial_memo:, đọc tốt hơn if not k in factorial_memo:.
ShreevatsaR

5
Nên thực sự làm điều này như một trang trí.
Emlyn O'Regan

3
@ durden2.0 Tôi biết đây là một nhận xét cũ, nhưng argslà một tuple. def some_function(*args)làm cho args một tuple.
Adam Smith

232

Mới dùng Python 3.2 functools.lru_cache. Theo mặc định, nó chỉ lưu trữ 128 cuộc gọi gần đây nhất được sử dụng, nhưng bạn có thể thiết lập maxsizeđể Nonechỉ ra rằng bộ nhớ cache không bao giờ nên hết hạn:

import functools

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

Chức năng này tự nó rất chậm, hãy thử fib(36)và bạn sẽ phải chờ khoảng mười giây.

Thêm lru_cachechú thích đảm bảo rằng nếu hàm được gọi gần đây cho một giá trị cụ thể, nó sẽ không tính toán lại giá trị đó, mà sử dụng kết quả được lưu trong bộ nhớ cache trước đó. Trong trường hợp này, nó dẫn đến một sự cải thiện tốc độ rất lớn, trong khi mã không bị lộn xộn với các chi tiết của bộ nhớ đệm.


2
Đã thử xơ (1000), đã nhận RecursionError: vượt quá độ sâu đệ quy tối đa vượt quá so với
X Æ A-12

5
@Andyk Giới hạn đệ quy Py3 mặc định là 1000. Lần đầu tiên fibđược gọi, nó sẽ cần phải lặp lại trường hợp cơ sở trước khi ghi nhớ có thể xảy ra. Vì vậy, hành vi của bạn chỉ là về dự kiến.
Quelklef

1
Nếu tôi không nhầm, nó chỉ lưu trữ cho đến khi quá trình không bị giết, phải không? Hoặc nó lưu cache bất kể quá trình bị giết? Giống như, giả sử tôi khởi động lại hệ thống của mình - các kết quả được lưu trong bộ nhớ cache vẫn được lưu trong bộ nhớ cache?
Kristada673

1
@ Kristada673 Có, nó được lưu trong bộ nhớ của tiến trình, không phải trên đĩa.
Flimm

2
Lưu ý rằng điều này tăng tốc ngay cả lần chạy đầu tiên của hàm, vì đây là hàm đệ quy và đang lưu trữ các kết quả trung gian của chính nó. Có thể là tốt để minh họa một chức năng không đệ quy vốn chỉ chậm chạp để làm cho nó rõ ràng hơn với những người giả như tôi. : D
endolith

61

Các câu trả lời khác bao gồm những gì nó là khá tốt. Tôi không nhắc lại điều đó. Chỉ cần một số điểm có thể hữu ích cho bạn.

Thông thường, phân biệt là một hoạt động bạn có thể áp dụng trên bất kỳ chức năng nào tính toán một cái gì đó (đắt tiền) và trả về một giá trị. Bởi vì điều này, nó thường được thực hiện như một trang trí . Việc thực hiện rất đơn giản và nó sẽ giống như thế này

memoised_function = memoise(actual_function)

hoặc thể hiện như một người trang trí

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

18

Ghi nhớ là giữ kết quả của các phép tính đắt tiền và trả về kết quả được lưu trong bộ nhớ cache thay vì liên tục tính toán lại.

Đây là một ví dụ:

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

Một mô tả đầy đủ hơn có thể được tìm thấy trong mục wikipedia về ghi nhớ .


Hmm, bây giờ nếu đó là Python chính xác, nó sẽ rung chuyển, nhưng dường như nó không ... ổn, vậy "bộ đệm" không phải là một lệnh? Bởi vì nếu đúng, nó phải if input not in self.cacheself.cache[input] ( has_keyđã lỗi thời kể từ ... đầu loạt 2.x, nếu không phải là 2.0. Không self.cache(index)bao giờ đúng. IIRC)
Jürgen A. Erhard

15

Chúng ta đừng quên hasattrchức năng tích hợp, dành cho những người muốn làm thủ công. Bằng cách đó, bạn có thể giữ bộ nhớ cache bên trong định nghĩa hàm (trái ngược với toàn cục).

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]

Đây dường như là một ý tưởng rất tốn kém. Với mỗi n, nó không chỉ lưu trữ kết quả cho n, mà còn cho 2 ... n-1.
tiền mã hóa

15

Tôi thấy điều này cực kỳ hữu ích

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)

Xem docs.python.org/3/l Library / funcools.html # funcools.wraps để biết lý do tại sao nên sử dụng functools.wraps.
anishpatel

1
Tôi có cần phải xóa thủ công memođể bộ nhớ được giải phóng không?
số

Toàn bộ ý tưởng là các kết quả được lưu trữ bên trong ghi nhớ trong một phiên. Tức là không có gì đang bị xóa như vậy
mr.bjerre

6

Ghi nhớ về cơ bản là lưu kết quả của các hoạt động trong quá khứ được thực hiện với các thuật toán đệ quy để giảm nhu cầu đi qua cây đệ quy nếu tính toán tương tự được yêu cầu ở giai đoạn sau.

xem http://scriptbucket.wordpress.com/2012/12/11/int sinhtion-to-memoization /

Ví dụ về ghi nhớ Fibonacci trong 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
Để có hiệu suất cao hơn, hãy tạo mầm cho bộ đệm xơ của bạn với một vài giá trị đã biết đầu tiên, sau đó bạn có thể sử dụng logic bổ sung để xử lý chúng ra khỏi 'đường dẫn nóng' của mã.
jkfending

5

Ghi nhớ là việc chuyển đổi các chức năng thành cấu trúc dữ liệu. Thông thường, người ta muốn chuyển đổi xảy ra tăng dần và lười biếng (theo yêu cầu của một yếu tố miền nhất định - hoặc "khóa"). Trong các ngôn ngữ chức năng lười biếng, việc chuyển đổi lười biếng này có thể xảy ra tự động và do đó việc ghi nhớ có thể được thực hiện mà không có tác dụng phụ (rõ ràng).


5

Vâng, tôi nên trả lời phần đầu tiên: ghi nhớ những gì?

Nó chỉ là một phương pháp để trao đổi bộ nhớ theo thời gian. Hãy nghĩ về bảng nhân .

Sử dụng đối tượng có thể thay đổi làm giá trị mặc định trong Python thường được coi là xấu. Nhưng nếu sử dụng nó một cách khôn ngoan, nó thực sự có thể hữu ích để thực hiện a memoization.

Dưới đây là một ví dụ được điều chỉnh từ http://docs.python.org/2/faq/design.html#why-are-default-values- Shared-b between-objects

Sử dụng một biến đổi dicttrong định nghĩa hàm, các kết quả tính toán trung gian có thể được lưu trữ (ví dụ: khi tính toán factorial(10)sau khi tính toán factorial(9), chúng ta có thể sử dụng lại tất cả các kết quả trung gian)

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

4

Đây là một giải pháp sẽ hoạt động với các đối số kiểu danh sách hoặc dict mà không rên rỉ:

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

Lưu ý rằng cách tiếp cận này có thể được mở rộng một cách tự nhiên cho bất kỳ đối tượng nào bằng cách triển khai hàm băm của riêng bạn như một trường hợp đặc biệt trong handle_item. Ví dụ: để làm cho cách tiếp cận này hoạt động đối với một hàm lấy một tập hợp làm đối số đầu vào, bạn có thể thêm vào handle_item:

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

1
Cố gắng tốt đẹp. Không có rên rỉ, một listđối số [1, 2, 3]có thể bị coi nhầm giống như một setđối số khác với giá trị là {1, 2, 3}. Ngoài ra, các bộ không có thứ tự như từ điển, vì vậy chúng cũng cần phải có sorted(). Cũng lưu ý rằng một đối số cấu trúc dữ liệu đệ quy sẽ gây ra một vòng lặp vô hạn.
martineau

Phải, các bộ nên được xử lý bằng cách xử lý vỏ đặc biệt_item (x) và sắp xếp. Tôi không nên nói rằng việc triển khai này xử lý các tập hợp, bởi vì nó không - nhưng vấn đề là nó có thể dễ dàng được mở rộng để thực hiện điều đó bằng cách xử lý vỏ đặc biệt, và điều tương tự sẽ hoạt động cho bất kỳ đối tượng lớp hoặc lặp nào miễn là bạn sẵn sàng tự viết hàm băm. Phần khó khăn - xử lý các danh sách hoặc từ điển đa chiều - đã được xử lý ở đây, vì vậy tôi thấy rằng chức năng ghi nhớ này dễ dàng hơn rất nhiều để làm việc với cơ sở so với các loại "Tôi chỉ lấy các đối số có thể băm" đơn giản.
RussellStewart

Vấn đề tôi đã đề cập là do thực tế là lists và sets bị "tupleized" vào cùng một thứ và do đó trở nên không thể phân biệt được với nhau. Mã ví dụ để thêm hỗ trợ setsđược mô tả trong bản cập nhật mới nhất của bạn không tránh khỏi điều đó tôi sợ. Điều này có thể dễ dàng được nhìn thấy bằng cách chuyển riêng [1,2,3]{1,2,3}làm đối số cho chức năng kiểm tra "ghi nhớ" và xem liệu nó có được gọi hai lần hay không, có nên hay không.
martineau

vâng, tôi đã đọc vấn đề đó, nhưng tôi đã không giải quyết nó bởi vì tôi nghĩ nó nhỏ hơn nhiều so với vấn đề khác mà bạn đề cập. Lần cuối cùng bạn viết một hàm ghi nhớ trong đó một đối số cố định có thể là danh sách hoặc tập hợp và hai kết quả dẫn đến kết quả đầu ra khác nhau? Nếu bạn gặp phải trường hợp hiếm gặp như vậy, bạn sẽ lại viết lại hand_item để trả trước, nói 0 nếu phần tử là tập hợp hoặc 1 nếu đó là danh sách.
RussellStewart

Trên thực tế, có một vấn đề tương tự với lists và dicts vì nó có thể cho một listđể có chính xác những điều tương tự trong nó dẫn từ gọi make_tuple(sorted(x.items()))cho một cuốn từ điển. Một giải pháp đơn giản cho cả hai trường hợp sẽ bao gồm type()giá trị trong bộ dữ liệu được tạo. Tôi có thể nghĩ ra một cách thậm chí đơn giản hơn để xử lý sets, nhưng nó không khái quát.
martineau

3

Giải pháp hoạt động với cả đối số vị trí và từ khóa độc lập theo thứ tự mà từ khóa đối số được thông qua (sử dụng tests.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

Câu hỏi tương tự: Xác định các hàm varargs tương đương gọi để ghi nhớ trong 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
bạn có thể sử dụng đơn giản if n not in cachethay thế. bằng cách sử dụng cache.keyssẽ xây dựng một danh sách không cần thiết trong python 2
n611x007

2

Chỉ muốn thêm vào các câu trả lời đã được cung cấp, thư viện trang trí Python có một số triển khai đơn giản nhưng hữu ích cũng có thể ghi nhớ "các loại không thể xóa", không giống như functools.lru_cache.


1
Nhà trang trí này không ghi nhớ "các loại không thể phá vỡ" ! Nó chỉ rơi trở lại để gọi chức năng mà không cần ghi nhớ, đi ngược lại với rõ ràng là tốt hơn so với giáo điều ngầm .
Ostrokach
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.